summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-06-09 15:09:21 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-06-09 15:09:21 +0000
commitd2675fa4de909714fcc6dc1bdd7bee9ce5e3af34 (patch)
tree0c0fbdd55fbb3a1616b10775113bf8320c9529c8
parent48d25238c386a89e8a6af218eeb290936a8f7595 (diff)
downloadgitlab-ce-d2675fa4de909714fcc6dc1bdd7bee9ce5e3af34.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.eslintrc.yml2
-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
-rw-r--r--config/feature_flags/development/container_registry_migration_phase2_delete_container_repository_worker_support.yml8
-rw-r--r--db/migrate/20220607095219_drop_ci_pipelines_config_pipeline_id_sequence.rb13
-rw-r--r--db/schema_migrations/202206070952191
-rw-r--r--db/structure.sql11
-rw-r--r--doc/administration/pages/index.md37
-rw-r--r--doc/ci/yaml/artifacts_reports.md1
-rw-r--r--doc/ci/yaml/index.md14
-rw-r--r--doc/update/index.md118
-rw-r--r--doc/update/package/index.md29
-rw-r--r--doc/user/gitlab_com/index.md2
-rw-r--r--doc/user/project/web_ide/index.md2
-rw-r--r--lib/api/terraform/modules/v1/packages.rb4
-rw-r--r--lib/container_registry/migration.rb8
-rw-r--r--lib/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences.rake1
-rw-r--r--locale/gitlab.pot57
-rw-r--r--qa/lib/gitlab/page/group/settings/usage_quotas.rb8
-rw-r--r--qa/qa/flow/purchase.rb2
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb28
-rw-r--r--spec/controllers/import/github_controller_spec.rb13
-rw-r--r--spec/frontend/__helpers__/init_vue_mr_page_helper.js18
-rw-r--r--spec/frontend/__helpers__/matchers/to_have_sprite_icon.js2
-rw-r--r--spec/frontend/admin/users/index_spec.js8
-rw-r--r--spec/frontend/authentication/two_factor_auth/index_spec.js4
-rw-r--r--spec/frontend/blob/components/table_contents_spec.js4
-rw-r--r--spec/frontend/blob/viewer/index_spec.js6
-rw-r--r--spec/frontend/cascading_settings/components/lock_popovers_spec.js10
-rw-r--r--spec/frontend/code_navigation/store/actions_spec.js12
-rw-r--r--spec/frontend/confirm_modal_spec.js6
-rw-r--r--spec/frontend/helpers/startup_css_helper_spec.js7
-rw-r--r--spec/frontend/issues/create_merge_request_dropdown_spec.js4
-rw-r--r--spec/frontend/labels/delete_label_modal_spec.js6
-rw-r--r--spec/frontend/lazy_loader_spec.js4
-rw-r--r--spec/frontend/members/index_spec.js2
-rw-r--r--spec/frontend/members/utils_spec.js2
-rw-r--r--spec/frontend/notebook/cells/markdown_spec.js4
-rw-r--r--spec/frontend/notes/stores/actions_spec.js10
-rw-r--r--spec/frontend/performance_bar/index_spec.js10
-rw-r--r--spec/frontend/search_autocomplete_spec.js2
-rw-r--r--spec/frontend/user_popovers_spec.js2
-rw-r--r--spec/frontend/users_select/test_helper.js8
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js192
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js43
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js113
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js40
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js46
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/mock_data.js30
-rw-r--r--spec/frontend_integration/ide/helpers/ide_helper.js4
-rw-r--r--spec/lib/container_registry/migration_spec.rb27
-rw-r--r--spec/models/group_spec.rb47
-rw-r--r--spec/models/members/last_group_owner_assigner_spec.rb13
-rw-r--r--spec/requests/api/terraform/modules/v1/packages_spec.rb47
-rw-r--r--spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb13
-rw-r--r--spec/workers/delete_container_repository_worker_spec.rb120
105 files changed, 1707 insertions, 276 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml
index 1dec5db472a..9f587bd204a 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -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*/**/*'
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}"
diff --git a/config/feature_flags/development/container_registry_migration_phase2_delete_container_repository_worker_support.yml b/config/feature_flags/development/container_registry_migration_phase2_delete_container_repository_worker_support.yml
new file mode 100644
index 00000000000..f6a5ae36c07
--- /dev/null
+++ b/config/feature_flags/development/container_registry_migration_phase2_delete_container_repository_worker_support.yml
@@ -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
diff --git a/db/migrate/20220607095219_drop_ci_pipelines_config_pipeline_id_sequence.rb b/db/migrate/20220607095219_drop_ci_pipelines_config_pipeline_id_sequence.rb
new file mode 100644
index 00000000000..fb7e14330cd
--- /dev/null
+++ b/db/migrate/20220607095219_drop_ci_pipelines_config_pipeline_id_sequence.rb
@@ -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
diff --git a/db/schema_migrations/20220607095219 b/db/schema_migrations/20220607095219
new file mode 100644
index 00000000000..e33cda54391
--- /dev/null
+++ b/db/schema_migrations/20220607095219
@@ -0,0 +1 @@
+64d492cca82603147226c9b0e6f424d2d2ba7a17ea0fe022510fb376016028e1 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index f4ddd923b85..08c8c4c7be3 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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);
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 425a1db27da..1535de0c2c2 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -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)**
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/16610) in GitLab 12.7.
-
-NOTE:
-Only GitLab administrators are able to view and override the **Maximum size of Pages** setting.
+## Set maximum size of each GitLab Pages site in a group **(PREMIUM SELF)**
-To override the global maximum pages size for a specific project:
+Prerequisites:
-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**.
+- You must have at least the Maintainer role for the group.
-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**.
diff --git a/doc/ci/yaml/artifacts_reports.md b/doc/ci/yaml/artifacts_reports.md
index 1a4733f7a2e..e4324ab06e1 100644
--- a/doc/ci/yaml/artifacts_reports.md
+++ b/doc/ci/yaml/artifacts_reports.md
@@ -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.
diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md
index 2f275e205af..e71bd8e57f0 100644
--- a/doc/ci/yaml/index.md
+++ b/doc/ci/yaml/index.md
@@ -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
diff --git a/doc/update/index.md b/doc/update/index.md
index 91813908629..a315d105edb 100644
--- a/doc/update/index.md
+++ b/doc/update/index.md
@@ -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
diff --git a/doc/update/package/index.md b/doc/update/package/index.md
index 8e90b949d24..47a7c1bf37c 100644
--- a/doc/update/package/index.md
+++ b/doc/update/package/index.md
@@ -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
+ ```
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index adccfc3d88b..31b6dba0ed5 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -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).
diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md
index b966bf855ce..731d09df089 100644
--- a/doc/user/project/web_ide/index.md
+++ b/doc/user/project/web_ide/index.md
@@ -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* -
diff --git a/lib/api/terraform/modules/v1/packages.rb b/lib/api/terraform/modules/v1/packages.rb
index 8da77ba18ae..f96cffb008c 100644
--- a/lib/api/terraform/modules/v1/packages.rb
+++ b/lib/api/terraform/modules/v1/packages.rb
@@ -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
diff --git a/lib/container_registry/migration.rb b/lib/container_registry/migration.rb
index 5c18e9066c1..92a001f9c24 100644
--- a/lib/container_registry/migration.rb
+++ b/lib/container_registry/migration.rb
@@ -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
diff --git a/lib/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences.rake b/lib/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences.rake
index afade62c6b6..a09f5195c62 100644
--- a/lib/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences.rake
+++ b/lib/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences.rake
@@ -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'
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ebec4a84cda..867b51e155f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8987,6 +8987,18 @@ msgstr ""
msgid "Collector hostname"
msgstr ""
+msgid "ColorWidget|An error occurred while updating color."
+msgstr ""
+
+msgid "ColorWidget|Assign epic color"
+msgstr ""
+
+msgid "ColorWidget|Color"
+msgstr ""
+
+msgid "ColorWidget|Error fetching epic color."
+msgstr ""
+
msgid "Colorize messages"
msgstr ""
@@ -11253,6 +11265,9 @@ msgstr ""
msgid "DORA4Metrics|Date"
msgstr ""
+msgid "DORA4Metrics|Days for an open incident"
+msgstr ""
+
msgid "DORA4Metrics|Days from merge to deploy"
msgstr ""
@@ -11265,6 +11280,15 @@ msgstr ""
msgid "DORA4Metrics|Median (last %{days}d)"
msgstr ""
+msgid "DORA4Metrics|Median time (last %{days}d)"
+msgstr ""
+
+msgid "DORA4Metrics|Median time an incident was open in a production environment over the given time period."
+msgstr ""
+
+msgid "DORA4Metrics|No incidents during this period"
+msgstr ""
+
msgid "DORA4Metrics|No merge requests were deployed during this period"
msgstr ""
@@ -11277,12 +11301,18 @@ msgstr ""
msgid "DORA4Metrics|Something went wrong while getting lead time data."
msgstr ""
+msgid "DORA4Metrics|Something went wrong while getting time to restore service data."
+msgstr ""
+
msgid "DORA4Metrics|The chart displays the frequency of deployments to production environment(s) that are based on the %{linkStart}deployment_tier%{linkEnd} value."
msgstr ""
msgid "DORA4Metrics|The chart displays the median time between a merge request being merged and deployed to production environment(s) that are based on the %{linkStart}deployment_tier%{linkEnd} value."
msgstr ""
+msgid "DORA4Metrics|Time to restore service"
+msgstr ""
+
msgid "DSN"
msgstr ""
@@ -17256,6 +17286,9 @@ msgstr ""
msgid "GitLabPages|Are you sure?"
msgstr ""
+msgid "GitLabPages|Can be overridden per project. For no limit, enter 0. To inherit the value, leave empty."
+msgstr ""
+
msgid "GitLabPages|Certificate: %{subject}"
msgstr ""
@@ -17277,7 +17310,7 @@ msgstr ""
msgid "GitLabPages|GitLab Pages are disabled for this project. You can enable them on your project's %{strong_start}Settings &gt; General &gt; Visibility%{strong_end} page."
msgstr ""
-msgid "GitLabPages|Maximum size of pages (MB)"
+msgid "GitLabPages|Maximum size (MB)"
msgstr ""
msgid "GitLabPages|New Domain"
@@ -17313,9 +17346,6 @@ msgstr ""
msgid "GitLabPages|Support for domains and certificates is disabled. Ask your system's administrator to enable it."
msgstr ""
-msgid "GitLabPages|The total size of deployed static content will be limited to this size. 0 for unlimited. Leave empty to inherit the global value."
-msgstr ""
-
msgid "GitLabPages|Unverified"
msgstr ""
@@ -18288,10 +18318,10 @@ msgstr ""
msgid "GroupSettings|Select the project that contains your custom Insights file."
msgstr ""
-msgid "GroupSettings|Set the initial name and protections for the default branch of new repositories created in the group."
+msgid "GroupSettings|Set a size limit for all content in each Pages site in this group. %{link_start}Learn more.%{link_end}"
msgstr ""
-msgid "GroupSettings|Set the maximum size of GitLab Pages for this group. %{link_start}Learn more.%{link_end}"
+msgid "GroupSettings|Set the initial name and protections for the default branch of new repositories created in the group."
msgstr ""
msgid "GroupSettings|The Auto DevOps pipeline runs if no alternative CI configuration file is found."
@@ -34068,6 +34098,15 @@ msgstr ""
msgid "SecurityOrchestration|This group"
msgstr ""
+msgid "SecurityOrchestration|This is a group-level policy"
+msgstr ""
+
+msgid "SecurityOrchestration|This is a project-level policy"
+msgstr ""
+
+msgid "SecurityOrchestration|This policy is inherited from %{namespace}"
+msgstr ""
+
msgid "SecurityOrchestration|This policy is inherited from the %{linkStart}namespace%{linkEnd} and must be edited there"
msgstr ""
@@ -36823,6 +36862,9 @@ msgstr ""
msgid "SuggestedColors|Gray"
msgstr ""
+msgid "SuggestedColors|Green"
+msgstr ""
+
msgid "SuggestedColors|Green screen"
msgstr ""
@@ -36841,6 +36883,9 @@ msgstr ""
msgid "SuggestedColors|Orange"
msgstr ""
+msgid "SuggestedColors|Purple"
+msgstr ""
+
msgid "SuggestedColors|Red"
msgstr ""
diff --git a/qa/lib/gitlab/page/group/settings/usage_quotas.rb b/qa/lib/gitlab/page/group/settings/usage_quotas.rb
index d19b2e43119..2b491188595 100644
--- a/qa/lib/gitlab/page/group/settings/usage_quotas.rb
+++ b/qa/lib/gitlab/page/group/settings/usage_quotas.rb
@@ -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
diff --git a/qa/qa/flow/purchase.rb b/qa/qa/flow/purchase.rb
index 1ea472a10fa..5558e177685 100644
--- a/qa/qa/flow/purchase.rb
+++ b/qa/qa/flow/purchase.rb
@@ -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
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index 25d32436d58..c6fd184ede0 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -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
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index ef66124bff1..56e55c45e66 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -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
diff --git a/spec/frontend/__helpers__/init_vue_mr_page_helper.js b/spec/frontend/__helpers__/init_vue_mr_page_helper.js
index ee01e9e6268..6b719a32480 100644
--- a/spec/frontend/__helpers__/init_vue_mr_page_helper.js
+++ b/spec/frontend/__helpers__/init_vue_mr_page_helper.js
@@ -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);
diff --git a/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js b/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js
index bce9d93bea8..45b9c31c4db 100644
--- a/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js
+++ b/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js
@@ -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);
diff --git a/spec/frontend/admin/users/index_spec.js b/spec/frontend/admin/users/index_spec.js
index 06dbadd6d3d..961fa96acdd 100644
--- a/spec/frontend/admin/users/index_spec.js
+++ b/spec/frontend/admin/users/index_spec.js
@@ -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));
});
diff --git a/spec/frontend/authentication/two_factor_auth/index_spec.js b/spec/frontend/authentication/two_factor_auth/index_spec.js
index 0ff9d60f409..f9a6b2df662 100644
--- a/spec/frontend/authentication/two_factor_auth/index_spec.js
+++ b/spec/frontend/authentication/two_factor_auth/index_spec.js
@@ -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());
diff --git a/spec/frontend/blob/components/table_contents_spec.js b/spec/frontend/blob/components/table_contents_spec.js
index 358ac31819c..2cbac809a0d 100644
--- a/spec/frontend/blob/components/table_contents_spec.js
+++ b/spec/frontend/blob/components/table_contents_spec.js
@@ -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);
diff --git a/spec/frontend/blob/viewer/index_spec.js b/spec/frontend/blob/viewer/index_spec.js
index 5f6baf3f63d..b2559af182b 100644
--- a/spec/frontend/blob/viewer/index_spec.js
+++ b/spec/frontend/blob/viewer/index_spec.js
@@ -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',
+ );
});
});
diff --git a/spec/frontend/cascading_settings/components/lock_popovers_spec.js b/spec/frontend/cascading_settings/components/lock_popovers_spec.js
index 585e6ac505b..182e3c1c8ff 100644
--- a/spec/frontend/cascading_settings/components/lock_popovers_spec.js
+++ b/spec/frontend/cascading_settings/components/lock_popovers_spec.js
@@ -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);
diff --git a/spec/frontend/code_navigation/store/actions_spec.js b/spec/frontend/code_navigation/store/actions_spec.js
index c47a9e697b6..8eee61d1342 100644
--- a/spec/frontend/code_navigation/store/actions_spec.js
+++ b/spec/frontend/code_navigation/store/actions_spec.js
@@ -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,
diff --git a/spec/frontend/confirm_modal_spec.js b/spec/frontend/confirm_modal_spec.js
index 53991349ee5..4224fb6be2a 100644
--- a/spec/frontend/confirm_modal_spec.js
+++ b/spec/frontend/confirm_modal_spec.js
@@ -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);
});
diff --git a/spec/frontend/helpers/startup_css_helper_spec.js b/spec/frontend/helpers/startup_css_helper_spec.js
index 2236b5aa261..05161437c22 100644
--- a/spec/frontend/helpers/startup_css_helper_spec.js
+++ b/spec/frontend/helpers/startup_css_helper_spec.js
@@ -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;
diff --git a/spec/frontend/issues/create_merge_request_dropdown_spec.js b/spec/frontend/issues/create_merge_request_dropdown_spec.js
index 20b26f5abba..cb7173c56a8 100644
--- a/spec/frontend/issues/create_merge_request_dropdown_spec.js
+++ b/spec/frontend/issues/create_merge_request_dropdown_spec.js
@@ -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();
diff --git a/spec/frontend/labels/delete_label_modal_spec.js b/spec/frontend/labels/delete_label_modal_spec.js
index 98049538948..67220821fe0 100644
--- a/spec/frontend/labels/delete_label_modal_spec.js
+++ b/spec/frontend/labels/delete_label_modal_spec.js
@@ -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';
diff --git a/spec/frontend/lazy_loader_spec.js b/spec/frontend/lazy_loader_spec.js
index 3d8b0d9c307..e0b6c7119f9 100644
--- a/spec/frontend/lazy_loader_spec.js
+++ b/spec/frontend/lazy_loader_spec.js
@@ -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');
});
diff --git a/spec/frontend/members/index_spec.js b/spec/frontend/members/index_spec.js
index efabe54f238..251a8b0b774 100644
--- a/spec/frontend/members/index_spec.js
+++ b/spec/frontend/members/index_spec.js
@@ -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 };
});
diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index a157cfa1c1d..b0c9459ff4f 100644
--- a/spec/frontend/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -256,7 +256,7 @@ describe('Members Utils', () => {
beforeEach(() => {
el = document.createElement('div');
- el.setAttribute('data-members-data', dataAttribute);
+ el.dataset.membersData = dataAttribute;
});
afterEach(() => {
diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js
index 7dc6f90d202..de415b5bfe0 100644
--- a/spec/frontend/notebook/cells/markdown_spec.js
+++ b/spec/frontend/notebook/cells/markdown_spec.js
@@ -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', () => {
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 4ecfbc5de1f..38f29ac2559 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -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', () => {
diff --git a/spec/frontend/performance_bar/index_spec.js b/spec/frontend/performance_bar/index_spec.js
index 008961bf709..2da176dbfe4 100644
--- a/spec/frontend/performance_bar/index_spec.js
+++ b/spec/frontend/performance_bar/index_spec.js
@@ -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);
diff --git a/spec/frontend/search_autocomplete_spec.js b/spec/frontend/search_autocomplete_spec.js
index 4639552b4d3..266f047e9dc 100644
--- a/spec/frontend/search_autocomplete_spec.js
+++ b/spec/frontend/search_autocomplete_spec.js
@@ -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.
diff --git a/spec/frontend/user_popovers_spec.js b/spec/frontend/user_popovers_spec.js
index 2c3db36d7e6..1544fed5240 100644
--- a/spec/frontend/user_popovers_spec.js
+++ b/spec/frontend/user_popovers_spec.js
@@ -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;
};
diff --git a/spec/frontend/users_select/test_helper.js b/spec/frontend/users_select/test_helper.js
index 59edde48eab..9231e38ea90 100644
--- a/spec/frontend/users_select/test_helper.js
+++ b/spec/frontend/users_select/test_helper.js
@@ -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;
}),
);
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
index 8efc4d84624..29ee7e0010f 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -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 () => {
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index a8c55c2c735..d134877e584 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -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');
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js
new file mode 100644
index 00000000000..fe614f03119
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js
@@ -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});`);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
new file mode 100644
index 00000000000..93b59800c27
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
@@ -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.',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js
new file mode 100644
index 00000000000..303824c77b3
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js
@@ -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);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
new file mode 100644
index 00000000000..74f50b878e2
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
@@ -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);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js
new file mode 100644
index 00000000000..d203d78477f
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js
@@ -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([[]]);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js
new file mode 100644
index 00000000000..f22592dd604
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js
@@ -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);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/mock_data.js b/spec/frontend/vue_shared/components/color_select_dropdown/mock_data.js
new file mode 100644
index 00000000000..097f47cc731
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/mock_data.js
@@ -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: [],
+ },
+ },
+};
diff --git a/spec/frontend_integration/ide/helpers/ide_helper.js b/spec/frontend_integration/ide/helpers/ide_helper.js
index 8c5ff816c74..0c3786929d8 100644
--- a/spec/frontend_integration/ide/helpers/ide_helper.js
+++ b/spec/frontend_integration/ide/helpers/ide_helper.js
@@ -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();
};
diff --git a/spec/lib/container_registry/migration_spec.rb b/spec/lib/container_registry/migration_spec.rb
index 8236ab0a70c..fea66d3c8f4 100644
--- a/spec/lib/container_registry/migration_spec.rb
+++ b/spec/lib/container_registry/migration_spec.rb
@@ -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
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index ab92606e6fc..d47f43a630d 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -991,6 +991,14 @@ RSpec.describe Group do
it { expect(group.last_owner?(@members[:owner])).to be_truthy }
+ context 'there is also a project_bot owner' do
+ before do
+ group.add_user(create(:user, :project_bot), GroupMember::OWNER)
+ end
+
+ it { expect(group.last_owner?(@members[:owner])).to be_truthy }
+ end
+
context 'with two owners' do
before do
create(:group_member, :owner, group: group)
@@ -1116,35 +1124,58 @@ RSpec.describe Group do
end
end
- describe '#single_owner?' do
+ describe '#all_owners_excluding_project_bots' do
let_it_be(:user) { create(:user) }
context 'when there is only one owner' do
- before do
+ let!(:owner) do
group.add_user(user, GroupMember::OWNER)
end
- it 'returns true' do
- expect(group.single_owner?).to eq(true)
+ it 'returns the owner' do
+ expect(group.all_owners_excluding_project_bots).to contain_exactly(owner)
+ end
+
+ context 'and there is also a project_bot owner' do
+ before do
+ group.add_user(create(:user, :project_bot), GroupMember::OWNER)
+ end
+
+ it 'returns only the human owner' do
+ expect(group.all_owners_excluding_project_bots).to contain_exactly(owner)
+ end
end
end
context 'when there are multiple owners' do
let_it_be(:user_2) { create(:user) }
- before do
+ let!(:owner) do
group.add_user(user, GroupMember::OWNER)
+ end
+
+ let!(:owner2) do
group.add_user(user_2, GroupMember::OWNER)
end
- it 'returns true' do
- expect(group.single_owner?).to eq(false)
+ it 'returns both owners' do
+ expect(group.all_owners_excluding_project_bots).to contain_exactly(owner, owner2)
+ end
+
+ context 'and there is also a project_bot owner' do
+ before do
+ group.add_user(create(:user, :project_bot), GroupMember::OWNER)
+ end
+
+ it 'returns only the human owners' do
+ expect(group.all_owners_excluding_project_bots).to contain_exactly(owner, owner2)
+ end
end
end
context 'when there are no owners' do
it 'returns false' do
- expect(group.single_owner?).to eq(false)
+ expect(group.all_owners_excluding_project_bots).to be_empty
end
end
end
diff --git a/spec/models/members/last_group_owner_assigner_spec.rb b/spec/models/members/last_group_owner_assigner_spec.rb
index bb0f751e7d5..429cf4190cf 100644
--- a/spec/models/members/last_group_owner_assigner_spec.rb
+++ b/spec/models/members/last_group_owner_assigner_spec.rb
@@ -94,5 +94,18 @@ RSpec.describe LastGroupOwnerAssigner do
end
end
end
+
+ context 'when there are bot members' do
+ context 'with a bot owner' do
+ specify do
+ create(:group_member, :owner, source: group, user: create(:user, :project_bot))
+
+ expect { assigner.execute }.to change(group_member, :last_owner)
+ .from(nil).to(true)
+ .and change(group_member, :last_blocked_owner)
+ .from(nil).to(false)
+ end
+ end
+ end
end
end
diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb
index 7d86244cb1b..f7736130245 100644
--- a/spec/requests/api/terraform/modules/v1/packages_spec.rb
+++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb
@@ -106,14 +106,14 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
context 'with valid namespace' do
where(:visibility, :user_role, :member, :token_type, :valid_token, :shared_examples_name, :expected_status) do
:public | :developer | true | :personal_access_token | true | 'grants terraform module download' | :success
- :public | :guest | true | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :guest | true | :personal_access_token | true | 'grants terraform module download' | :success
:public | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
:public | :guest | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
- :public | :developer | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
- :public | :guest | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :developer | false | :personal_access_token | true | 'grants terraform module download' | :success
+ :public | :guest | false | :personal_access_token | true | 'grants terraform module download' | :success
:public | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
:public | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
- :public | :anonymous | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :anonymous | false | :anonymous | true | 'grants terraform module download' | :success
:private | :developer | true | :personal_access_token | true | 'grants terraform module download' | :success
:private | :guest | true | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
:private | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
@@ -122,12 +122,12 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
:private | :guest | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
:private | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
:private | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
- :private | :anonymous | false | :personal_access_token | true | 'rejects terraform module packages access' | :unauthorized
+ :private | :anonymous | false | :anonymous | true | 'rejects terraform module packages access' | :unauthorized
:public | :developer | true | :job_token | true | 'grants terraform module download' | :success
- :public | :guest | true | :job_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :guest | true | :job_token | true | 'grants terraform module download' | :success
:public | :guest | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
- :public | :developer | false | :job_token | true | 'rejects terraform module packages access' | :not_found
- :public | :guest | false | :job_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :developer | false | :job_token | true | 'grants terraform module download' | :success
+ :public | :guest | false | :job_token | true | 'grants terraform module download' | :success
:public | :developer | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
:public | :guest | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
:private | :developer | true | :job_token | true | 'grants terraform module download' | :success
@@ -146,6 +146,7 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
before do
group.update!(visibility: visibility.to_s)
+ project.update!(visibility: visibility.to_s)
end
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
@@ -158,7 +159,8 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
let(:tokens) do
{
personal_access_token: ::Gitlab::JWTToken.new.tap { |jwt| jwt['token'] = personal_access_token.id }.encoded,
- job_token: ::Gitlab::JWTToken.new.tap { |jwt| jwt['token'] = job.token }.encoded
+ job_token: ::Gitlab::JWTToken.new.tap { |jwt| jwt['token'] = job.token }.encoded,
+ anonymous: ""
}
end
@@ -167,14 +169,14 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
context 'with valid namespace' do
where(:visibility, :user_role, :member, :token_type, :valid_token, :shared_examples_name, :expected_status) do
:public | :developer | true | :personal_access_token | true | 'grants terraform module package file access' | :success
- :public | :guest | true | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :guest | true | :personal_access_token | true | 'grants terraform module package file access' | :success
:public | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
:public | :guest | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
- :public | :developer | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
- :public | :guest | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :developer | false | :personal_access_token | true | 'grants terraform module package file access' | :success
+ :public | :guest | false | :personal_access_token | true | 'grants terraform module package file access' | :success
:public | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
:public | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
- :public | :anonymous | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :anonymous | false | :anonymous | true | 'grants terraform module package file access' | :success
:private | :developer | true | :personal_access_token | true | 'grants terraform module package file access' | :success
:private | :guest | true | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
:private | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
@@ -183,12 +185,12 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
:private | :guest | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
:private | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
:private | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
- :private | :anonymous | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :anonymous | false | :anonymous | true | 'rejects terraform module packages access' | :unauthorized
:public | :developer | true | :job_token | true | 'grants terraform module package file access' | :success
- :public | :guest | true | :job_token | true | 'rejects terraform module packages access' | :not_found
- :public | :guest | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
- :public | :developer | false | :job_token | true | 'rejects terraform module packages access' | :not_found
- :public | :guest | false | :job_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :guest | true | :job_token | true | 'grants terraform module package file access' | :success
+ :public | :guest | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | false | :job_token | true | 'grants terraform module package file access' | :success
+ :public | :guest | false | :job_token | true | 'grants terraform module package file access' | :success
:public | :developer | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
:public | :guest | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
:private | :developer | true | :job_token | true | 'grants terraform module package file access' | :success
@@ -203,10 +205,17 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
with_them do
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
- let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace } }
+ let(:snowplow_gitlab_standard_context) do
+ {
+ project: project,
+ user: user_role == :anonymous ? nil : user,
+ namespace: project.namespace
+ }
+ end
before do
group.update!(visibility: visibility.to_s)
+ project.update!(visibility: visibility.to_s)
end
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
index 2ea98002de1..5faf462c23c 100644
--- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
@@ -36,6 +36,19 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST personal_access_toke
expect(session[:"#{provider}_access_token"]).to eq(token)
expect(controller).to redirect_to(status_import_url)
end
+
+ 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
RSpec.shared_examples 'a GitHub-ish import controller: GET new' do
diff --git a/spec/workers/delete_container_repository_worker_spec.rb b/spec/workers/delete_container_repository_worker_spec.rb
index ec040eab2d4..a011457444a 100644
--- a/spec/workers/delete_container_repository_worker_spec.rb
+++ b/spec/workers/delete_container_repository_worker_spec.rb
@@ -3,31 +3,119 @@
require 'spec_helper'
RSpec.describe DeleteContainerRepositoryWorker do
- let(:registry) { create(:container_repository) }
- let(:project) { registry.project }
- let(:user) { project.first_owner }
+ let_it_be(:repository) { create(:container_repository) }
- subject { described_class.new }
+ let(:project) { repository.project }
+ let(:user) { project.first_owner }
+ let(:worker) { described_class.new }
describe '#perform' do
+ let(:user_id) { user.id }
+ let(:repository_id) { repository.id }
+
+ subject(:perform) { worker.perform(user_id, repository_id) }
+
it 'executes the destroy service' do
- service = instance_double(Projects::ContainerRepository::DestroyService)
- expect(service).to receive(:execute)
- expect(Projects::ContainerRepository::DestroyService).to receive(:new).with(project, user).and_return(service)
+ expect_destroy_service_execution
+
+ perform
+ end
+
+ context 'with an invalid user id' do
+ let(:user_id) { -1 }
+
+ it { expect { perform }.not_to raise_error }
+ end
- subject.perform(user.id, registry.id)
+ context 'with an invalid repository id' do
+ let(:repository_id) { -1 }
+
+ it { expect { perform }.not_to raise_error }
end
- it 'does not raise error when user could not be found' do
- expect do
- subject.perform(-1, registry.id)
- end.not_to raise_error
+ context 'with a repository being migrated', :freeze_time do
+ before do
+ stub_application_setting(
+ container_registry_pre_import_tags_rate: 0.5,
+ container_registry_import_timeout: 10.minutes.to_i
+ )
+ end
+
+ shared_examples 'destroying the repository' do
+ it 'does destroy the repository' do
+ expect_next_found_instance_of(ContainerRepository) do |container_repository|
+ expect(container_repository).not_to receive(:tags_count)
+ end
+ expect(described_class).not_to receive(:perform_in)
+ expect_destroy_service_execution
+
+ perform
+ end
+ end
+
+ shared_examples 'not re enqueuing job if feature flag is disabled' do
+ before do
+ stub_feature_flags(container_registry_migration_phase2_delete_container_repository_worker_support: false)
+ end
+
+ it_behaves_like 'destroying the repository'
+ end
+
+ context 'with migration state set to pre importing' do
+ let_it_be(:repository) { create(:container_repository, :pre_importing) }
+
+ let(:tags_count) { 60 }
+ let(:delay) { (tags_count * 0.5).seconds + 10.minutes + described_class::FIXED_DELAY }
+
+ it 'does not destroy the repository and re enqueue the job' do
+ expect_next_found_instance_of(ContainerRepository) do |container_repository|
+ expect(container_repository).to receive(:tags_count).and_return(tags_count)
+ end
+ expect(described_class).to receive(:perform_in).with(delay.from_now)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:delete_postponed, delay)
+ expect(::Projects::ContainerRepository::DestroyService).not_to receive(:new)
+
+ perform
+ end
+
+ it_behaves_like 'not re enqueuing job if feature flag is disabled'
+ end
+
+ %i[pre_import_done importing import_aborted].each do |migration_state|
+ context "with migration state set to #{migration_state}" do
+ let_it_be(:repository) { create(:container_repository, migration_state) }
+
+ let(:delay) { 10.minutes + described_class::FIXED_DELAY }
+
+ it 'does not destroy the repository and re enqueue the job' do
+ expect_next_found_instance_of(ContainerRepository) do |container_repository|
+ expect(container_repository).not_to receive(:tags_count)
+ end
+ expect(described_class).to receive(:perform_in).with(delay.from_now)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:delete_postponed, delay)
+ expect(::Projects::ContainerRepository::DestroyService).not_to receive(:new)
+
+ perform
+ end
+
+ it_behaves_like 'not re enqueuing job if feature flag is disabled'
+ end
+ end
+
+ %i[default import_done import_skipped].each do |migration_state|
+ context "with migration state set to #{migration_state}" do
+ let_it_be(:repository) { create(:container_repository, migration_state) }
+
+ it_behaves_like 'destroying the repository'
+ it_behaves_like 'not re enqueuing job if feature flag is disabled'
+ end
+ end
end
- it 'does not raise error when registry could not be found' do
- expect do
- subject.perform(user.id, -1)
- end.not_to raise_error
+ def expect_destroy_service_execution
+ service = instance_double(Projects::ContainerRepository::DestroyService)
+ expect(service).to receive(:execute)
+ expect(Projects::ContainerRepository::DestroyService).to receive(:new).with(project, user).and_return(service)
end
end
end