Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-06-09 18:09:21 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-06-09 18:09:21 +0300
commitd2675fa4de909714fcc6dc1bdd7bee9ce5e3af34 (patch)
tree0c0fbdd55fbb3a1616b10775113bf8320c9529c8 /app
parent48d25238c386a89e8a6af218eeb290936a8f7595 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_kroki.js4
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_math.js4
-rw-r--r--app/assets/javascripts/blob/blob_line_permalink_updater.js5
-rw-r--r--app/assets/javascripts/blob/viewer/index.js17
-rw-r--r--app/assets/javascripts/breadcrumb.js2
-rw-r--r--app/assets/javascripts/code_navigation/utils/index.js4
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/render.js8
-rw-r--r--app/assets/javascripts/diff.js2
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_operator.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js2
-rw-r--r--app/assets/javascripts/filtered_search/droplab/drop_down.js4
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js2
-rw-r--r--app/assets/javascripts/image_diff/helpers/dom_helper.js2
-rw-r--r--app/assets/javascripts/issues/create_merge_request_dropdown.js5
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue2
-rw-r--r--app/assets/javascripts/lazy_loader.js9
-rw-r--r--app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js2
-rw-r--r--app/assets/javascripts/members/components/table/role_dropdown.vue2
-rw-r--r--app/assets/javascripts/pages/shared/nav/sidebar_tracking.js10
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js2
-rw-r--r--app/assets/javascripts/projects/commits/components/author_select.vue2
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue3
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js4
-rw-r--r--app/assets/javascripts/terraform/index.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/color_item.vue25
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue214
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js30
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents.vue109
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue53
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_header.vue31
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_value.vue43
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql9
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql9
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/utils.js15
-rw-r--r--app/assets/javascripts/whats_new/components/app.vue2
-rw-r--r--app/assets/javascripts/whats_new/utils/notification.js2
-rw-r--r--app/controllers/import/gitea_controller.rb2
-rw-r--r--app/models/group.rb7
-rw-r--r--app/models/members/last_group_owner_assigner.rb3
-rw-r--r--app/views/admin/application_settings/_pages.html.haml2
-rw-r--r--app/views/import/gitea/new.html.haml1
-rw-r--r--app/views/import/gitea/status.html.haml2
-rw-r--r--app/views/projects/_import_project_pane.html.haml2
-rw-r--r--app/workers/container_registry/migration/guard_worker.rb6
-rw-r--r--app/workers/delete_container_repository_worker.rb37
48 files changed, 645 insertions, 72 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/render_kroki.js b/app/assets/javascripts/behaviors/markdown/render_kroki.js
index abe71694d73..241585c30f1 100644
--- a/app/assets/javascripts/behaviors/markdown/render_kroki.js
+++ b/app/assets/javascripts/behaviors/markdown/render_kroki.js
@@ -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));
}
});
diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js
index fd1a99acf99..af7aac4cf36 100644
--- a/app/assets/javascripts/behaviors/markdown/render_math.js
+++ b/app/assets/javascripts/behaviors/markdown/render_math.js
@@ -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);
diff --git a/app/assets/javascripts/blob/blob_line_permalink_updater.js b/app/assets/javascripts/blob/blob_line_permalink_updater.js
index a3dd241604d..0a5bcf326a1 100644
--- a/app/assets/javascripts/blob/blob_line_permalink_updater.js
+++ b/app/assets/javascripts/blob/blob_line_permalink_updater.js
@@ -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}`);
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index a6eed4ecae3..a0d4f7ef4f2 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -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);
});
diff --git a/app/assets/javascripts/breadcrumb.js b/app/assets/javascripts/breadcrumb.js
index b9d3742974c..113840dbc52 100644
--- a/app/assets/javascripts/breadcrumb.js
+++ b/app/assets/javascripts/breadcrumb.js
@@ -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');
}
};
diff --git a/app/assets/javascripts/code_navigation/utils/index.js b/app/assets/javascripts/code_navigation/utils/index.js
index 0d72153d8fe..46038df2f86 100644
--- a/app/assets/javascripts/code_navigation/utils/index.js
+++ b/app/assets/javascripts/code_navigation/utils/index.js
@@ -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');
}
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/render.js b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
index 37287b9d981..f10c2d82b61 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/render.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
@@ -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);
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index a12829f8420..47de7a76899 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -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) {
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index b57db73a86e..3913e4e8d81 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -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 || '';
}
}
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
index 9d29782c9a7..93897b4ed6c 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -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 {
diff --git a/app/assets/javascripts/filtered_search/dropdown_operator.js b/app/assets/javascripts/filtered_search/dropdown_operator.js
index fb9f25a8c45..cd0f541b4fb 100644
--- a/app/assets/javascripts/filtered_search/dropdown_operator.js
+++ b/app/assets/javascripts/filtered_search/dropdown_operator.js
@@ -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;
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index 9a23ff25eac..26507a85fa8 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -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() {
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index c98d1f8e064..22e1604871a 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -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'),
},
});
diff --git a/app/assets/javascripts/filtered_search/droplab/drop_down.js b/app/assets/javascripts/filtered_search/droplab/drop_down.js
index 05b741af191..398a7b26677 100644
--- a/app/assets/javascripts/filtered_search/droplab/drop_down.js
+++ b/app/assets/javascripts/filtered_search/droplab/drop_down.js
@@ -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;
});
}
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 07f2c75f00a..ac2cf27e873 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -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;
});
diff --git a/app/assets/javascripts/image_diff/helpers/dom_helper.js b/app/assets/javascripts/image_diff/helpers/dom_helper.js
index 3468a629f5a..180e927a3e7 100644
--- a/app/assets/javascripts/image_diff/helpers/dom_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/dom_helper.js
@@ -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) {
diff --git a/app/assets/javascripts/issues/create_merge_request_dropdown.js b/app/assets/javascripts/issues/create_merge_request_dropdown.js
index c5f31081625..edf3789e6dc 100644
--- a/app/assets/javascripts/issues/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/issues/create_merge_request_dropdown.js
@@ -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();
}
}
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index d6ea06d172e..90e039259da 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -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],
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
index 2b4dd205cf1..ba801082377 100644
--- a/app/assets/javascripts/lazy_loader.js
+++ b/app/assets/javascripts/lazy_loader.js
@@ -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');
diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js
index 173116062c9..2dc479db80a 100644
--- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js
+++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js
@@ -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;
diff --git a/app/assets/javascripts/members/components/table/role_dropdown.vue b/app/assets/javascripts/members/components/table/role_dropdown.vue
index fa895cf24c4..6cd8bf57313 100644
--- a/app/assets/javascripts/members/components/table/role_dropdown.vue
+++ b/app/assets/javascripts/members/components/table/role_dropdown.vue
@@ -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: {
diff --git a/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js b/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js
index 79ce1a37d21..47aae36ecbb 100644
--- a/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js
+++ b/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js
@@ -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';
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 996e12bc105..94506d33b33 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -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(() =>
diff --git a/app/assets/javascripts/projects/commits/components/author_select.vue b/app/assets/javascripts/projects/commits/components/author_select.vue
index c8a0a3417f3..884ef732144 100644
--- a/app/assets/javascripts/projects/commits/components/author_select.vue
+++ b/app/assets/javascripts/projects/commits/components/author_select.vue
@@ -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;
}
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
index d4b1f7e57d8..0c4ce5dab65 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
@@ -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) {
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 351bb50d941..81ed04c7ce6 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -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', {
diff --git a/app/assets/javascripts/terraform/index.js b/app/assets/javascripts/terraform/index.js
index 571177986d2..1f851a4376a 100644
--- a/app/assets/javascripts/terraform/index.js
+++ b/app/assets/javascripts/terraform/index.js
@@ -39,7 +39,7 @@ export default () => {
return createElement(TerraformList, {
props: {
emptyStateImage,
- terraformAdmin: el.hasAttribute('data-terraform-admin'),
+ terraformAdmin: Object.hasOwn(el.dataset, 'terraformAdmin'),
},
});
},
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_item.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_item.vue
new file mode 100644
index 00000000000..92817d5fa70
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_item.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue
new file mode 100644
index 00000000000..6b79883d76b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js b/app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js
new file mode 100644
index 00000000000..c70785abd1e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js
@@ -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',
+ },
+];
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents.vue
new file mode 100644
index 00000000000..4eb1d3d08ca
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue
new file mode 100644
index 00000000000..62f4cf59c14
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_header.vue
new file mode 100644
index 00000000000..a32b1570f5f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_header.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_value.vue
new file mode 100644
index 00000000000..4cba66eefd2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_value.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql
new file mode 100644
index 00000000000..959e0f8c1a5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql
@@ -0,0 +1,9 @@
+query epicColor($fullPath: ID!, $iid: ID) {
+ workspace: group(fullPath: $fullPath) {
+ id
+ issuable: epic(iid: $iid) {
+ id
+ color
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql
new file mode 100644
index 00000000000..2975b42253f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql
@@ -0,0 +1,9 @@
+mutation updateEpicColor($input: UpdateEpicInput!) {
+ updateIssuableColor: updateEpic(input: $input) {
+ issuable: epic {
+ id
+ color
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/utils.js b/app/assets/javascripts/vue_shared/components/color_select_dropdown/utils.js
new file mode 100644
index 00000000000..46196e793b3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/utils.js
@@ -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;
diff --git a/app/assets/javascripts/whats_new/components/app.vue b/app/assets/javascripts/whats_new/components/app.vue
index b74dba686ad..0c55cc2f8a6 100644
--- a/app/assets/javascripts/whats_new/components/app.vue
+++ b/app/assets/javascripts/whats_new/components/app.vue
@@ -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 });
},
diff --git a/app/assets/javascripts/whats_new/utils/notification.js b/app/assets/javascripts/whats_new/utils/notification.js
index 66ee3b1a971..41aff202f48 100644
--- a/app/assets/javascripts/whats_new/utils/notification.js
+++ b/app/assets/javascripts/whats_new/utils/notification.js
@@ -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);
diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb
index 4b4ac07b389..399a92c59e0 100644
--- a/app/controllers/import/gitea_controller.rb
+++ b/app/controllers/import/gitea_controller.rb
@@ -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
diff --git a/app/models/group.rb b/app/models/group.rb
index 5369e873d17..aa810b62d7f 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -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?
diff --git a/app/models/members/last_group_owner_assigner.rb b/app/models/members/last_group_owner_assigner.rb
index dcf0a2d0ad3..c85116858c7 100644
--- a/app/models/members/last_group_owner_assigner.rb
+++ b/app/models/members/last_group_owner_assigner.rb
@@ -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
diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml
index 74903d52f25..5ef8a24ba39 100644
--- a/app/views/admin/application_settings/_pages.html.haml
+++ b/app/views/admin/application_settings/_pages.html.haml
@@ -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
diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml
index a7feedd91ad..4a293bb6f4e 100644
--- a/app/views/import/gitea/new.html.haml
+++ b/app/views/import/gitea/new.html.haml
@@ -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
diff --git a/app/views/import/gitea/status.html.haml b/app/views/import/gitea/status.html.haml
index 2e318f77b7c..c717d4848f4 100644
--- a/app/views/import/gitea/status.html.haml
+++ b/app/views/import/gitea/status.html.haml
@@ -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
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
index c60124ecac0..cb15858a935 100644
--- a/app/views/projects/_import_project_pane.html.haml
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -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
diff --git a/app/workers/container_registry/migration/guard_worker.rb b/app/workers/container_registry/migration/guard_worker.rb
index 1b183680796..ae29106b502 100644
--- a/app/workers/container_registry/migration/guard_worker.rb
+++ b/app/workers/container_registry/migration/guard_worker.rb
@@ -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
diff --git a/app/workers/delete_container_repository_worker.rb b/app/workers/delete_container_repository_worker.rb
index a4d6adc2195..73e6843fdd0 100644
--- a/app/workers/delete_container_repository_worker.rb
+++ b/app/workers/delete_container_repository_worker.rb
@@ -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}"