diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-12 00:08:09 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-12 00:08:09 +0300 |
commit | fdc26e021b1e3eea4161bf6891f3a151fb7414b0 (patch) | |
tree | f06ce58930f41f8d031e827df198fed5dfab09be /app | |
parent | 11df4bf91b8cf9ac7bb601241992e300eebf684c (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
61 files changed, 271 insertions, 384 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/render_kroki.js b/app/assets/javascripts/behaviors/markdown/render_kroki.js index abe71694d73..241585c30f1 100644 --- a/app/assets/javascripts/behaviors/markdown/render_kroki.js +++ b/app/assets/javascripts/behaviors/markdown/render_kroki.js @@ -55,8 +55,8 @@ export function renderKroki(krokiImages) { // A single Kroki image is processed multiple times for some reason, // so this condition ensures we only create one alert per Kroki image - if (!parent.hasAttribute('data-kroki-processed')) { - parent.setAttribute('data-kroki-processed', 'true'); + if (!Object.hasOwn(parent.dataset, 'krokiProcessed')) { + parent.dataset.krokiProcessed = 'true'; parent.after(createAlert(krokiImage)); } }); diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js index 12f47255bdf..7e1bf83f8a1 100644 --- a/app/assets/javascripts/behaviors/markdown/render_math.js +++ b/app/assets/javascripts/behaviors/markdown/render_math.js @@ -110,7 +110,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, @@ -143,7 +143,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 3ea70c07058..7c611dbf39e 100644 --- a/app/assets/javascripts/issues/create_merge_request_dropdown.js +++ b/app/assets/javascripts/issues/create_merge_request_dropdown.js @@ -81,10 +81,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 831cef66836..7594676489f 100644 --- a/app/assets/javascripts/issues/show/components/description.vue +++ b/app/assets/javascripts/issues/show/components/description.vue @@ -270,7 +270,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 1adb6f9c26f..a0e730e9722 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 @@ -52,7 +52,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/registrations/new/index.js b/app/assets/javascripts/pages/registrations/new/index.js index 8bbe81a9ed5..94a5c1cb29b 100644 --- a/app/assets/javascripts/pages/registrations/new/index.js +++ b/app/assets/javascripts/pages/registrations/new/index.js @@ -3,9 +3,14 @@ import { trackNewRegistrations } from '~/google_tag_manager'; import NoEmojiValidator from '~/emoji/no_emoji_validator'; import LengthValidator from '~/pages/sessions/new/length_validator'; import UsernameValidator from '~/pages/sessions/new/username_validator'; +import Tracking from '~/tracking'; new UsernameValidator(); // eslint-disable-line no-new new LengthValidator(); // eslint-disable-line no-new new NoEmojiValidator(); // eslint-disable-line no-new trackNewRegistrations(); + +Tracking.enableFormTracking({ + forms: { allow: ['new_user'] }, +}); 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/pipeline_editor/components/header/pipeline_editor_mini_graph.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue index 1f2fdb7ce36..7beabcfe403 100644 --- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue +++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue @@ -47,6 +47,12 @@ export default { downstreamPipelines() { return this.linkedPipelines?.downstream?.nodes || []; }, + hasDownstreamPipelines() { + return this.downstreamPipelines.length > 0; + }, + hasPipelineStages() { + return this.pipelineStages.length > 0; + }, pipelinePath() { return this.pipeline.detailedStatus?.detailsPath || ''; }, @@ -73,9 +79,6 @@ export default { }; }); }, - showDownstreamPipelines() { - return this.downstreamPipelines.length > 0; - }, upstreamPipeline() { return this.linkedPipelines?.upstream; }, @@ -84,7 +87,10 @@ export default { </script> <template> - <div v-if="pipelineStages.length > 0" class="stage-cell gl-mr-5"> + <div + v-if="hasPipelineStages" + class="gl-align-items-center gl-display-inline-flex gl-flex-wrap stage-cell gl-mr-5" + > <linked-pipelines-mini-list v-if="upstreamPipeline" :triggered-by="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [ @@ -92,9 +98,9 @@ export default { ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */" data-testid="pipeline-editor-mini-graph-upstream" /> - <pipeline-mini-graph class="gl-display-inline" :stages="pipelineStages" /> + <pipeline-mini-graph :stages="pipelineStages" /> <linked-pipelines-mini-list - v-if="showDownstreamPipelines" + v-if="hasDownstreamPipelines" :triggered="downstreamPipelines" :pipeline-path="pipelinePath" data-testid="pipeline-editor-mini-graph-downstream" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue index 2b33467e948..6ed9d8d4dc9 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue @@ -36,7 +36,7 @@ export default { }; </script> <template> - <div data-testid="pipeline-mini-graph"> + <div data-testid="pipeline-mini-graph" class="gl-display-inline-flex gl-flex-wrap gl-my-1"> <div v-for="stage in stages" :key="stage.name" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue index 23e2a36ddeb..53e21d4ce8b 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue @@ -12,7 +12,8 @@ * 4. Commit widget */ -import { GlDropdown, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui'; +import { GlDropdown, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { __, sprintf } from '~/locale'; @@ -21,7 +22,7 @@ import JobItem from './job_item.vue'; export default { components: { - GlIcon, + CiIcon, GlLoadingIcon, GlDropdown, JobItem, @@ -51,14 +52,6 @@ export default { dropdownContent: [], }; }, - computed: { - triggerButtonClass() { - return `ci-status-icon-${this.stage.status.group}`; - }, - borderlessIcon() { - return `${this.stage.status.icon}_borderless`; - }, - }, watch: { updateDropdown() { if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) { @@ -117,14 +110,18 @@ export default { :popper-opts="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ { placement: 'bottom', } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */" - :toggle-class="['mini-pipeline-graph-dropdown-toggle', triggerButtonClass]" + :toggle-class="['gl-rounded-full!']" menu-class="mini-pipeline-graph-dropdown-menu" @show="onShowDropdown" > <template #button-content> - <span class="gl-pointer-events-none"> - <gl-icon :name="borderlessIcon" /> - </span> + <ci-icon + is-interactive + css-classes="gl-rounded-full" + :size="24" + :status="stage.status" + class="gl-align-items-center gl-display-inline-flex" + /> </template> <gl-loading-icon v-if="isLoading" size="sm" /> <ul diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue index d77c0a39bea..5fe7c48b380 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue @@ -169,7 +169,7 @@ export default { </template> <template #cell(stages)="{ item }"> - <div class="stage-cell"> + <div class="gl-align-items-center gl-display-inline-flex gl-flex-wrap stage-cell"> <!-- This empty div should be removed, see https://gitlab.com/gitlab-org/gitlab/-/issues/323488 --> <div></div> <linked-pipelines-mini-list @@ -181,7 +181,6 @@ export default { /> <pipeline-mini-graph v-if="item.details && item.details.stages && item.details.stages.length > 0" - class="gl-display-inline" :stages="item.details.stages" :update-dropdown="updateGraphDropdown" @pipelineActionRequestComplete="onPipelineActionRequestComplete" diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue index efac9f3044a..6ed943d7df5 100644 --- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue +++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue @@ -123,7 +123,7 @@ export default { <template> <div> <gl-loading-icon v-if="$apollo.queries.pipeline.loading" /> - <div v-else> + <div v-else class="gl-align-items-center gl-display-flex"> <linked-pipelines-mini-list v-if="upstreamPipeline" :triggered-by="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [ @@ -132,11 +132,7 @@ export default { data-testid="commit-box-mini-graph-upstream" /> - <pipeline-mini-graph - :stages="formattedStages" - class="gl-display-inline" - data-testid="commit-box-mini-graph" - /> + <pipeline-mini-graph :stages="formattedStages" data-testid="commit-box-mini-graph" /> <linked-pipelines-mini-list v-if="hasDownstream" 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/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 2a7d967cb61..5e57d4c3d6e 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 34261f3c4db..64e90f9227c 100644 --- a/app/assets/javascripts/terraform/index.js +++ b/app/assets/javascripts/terraform/index.js @@ -39,7 +39,7 @@ export default () => { props: { emptyStateImage, projectPath, - terraformAdmin: el.hasAttribute('data-terraform-admin'), + terraformAdmin: Object.hasOwn(el.dataset, 'terraformAdmin'), }, }); }, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index c0b80eef082..3b3b46e9772 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -276,12 +276,11 @@ export default { </div> </div> <div> - <span class="mr-widget-pipeline-graph"> - <span class="stage-cell"> + <span class="gl-align-items-center gl-display-inline-flex mr-widget-pipeline-graph"> + <span class="gl-align-items-center gl-display-inline-flex gl-flex-wrap stage-cell"> <linked-pipelines-mini-list v-if="triggeredBy.length" :triggered-by="triggeredBy" /> <pipeline-mini-graph v-if="hasStages" - class="gl-display-inline-block" stages-class="mr-widget-pipeline-stages" :stages="pipeline.details.stages" :is-merge-train="isMergeTrain" diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index 07bd6019b80..9bccc49e894 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -11,17 +11,22 @@ import { GlIcon } from '@gitlab/ui'; * } * * Used in: - * - Pipelines table Badge - * - Pipelines table mini graph - * - Pipeline graph - * - Pipeline show view badge - * - Jobs table + * - Extended MR Popover * - Jobs show view header * - Jobs show view sidebar + * - Jobs table * - Linked pipelines - * - Extended MR Popover + * - Pipeline graph + * - Pipeline mini graph + * - Pipeline show view badge + * - Pipelines table Badge + */ + +/* + * These sizes are defined in gitlab-ui/src/scss/variables.scss + * under '$gl-icon-sizes' */ -const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; +const validSizes = [8, 12, 14, 16, 24, 32, 48, 72]; export default { components: { @@ -45,6 +50,11 @@ export default { required: false, default: false, }, + isInteractive: { + type: Boolean, + required: false, + default: false, + }, cssClasses: { type: String, required: false, @@ -52,9 +62,9 @@ export default { }, }, computed: { - cssClass() { + wrapperStyleClasses() { const status = this.status.group; - return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`; + return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status} gl-rounded-full gl-justify-content-center`; }, icon() { return this.borderless ? `${this.status.icon}_borderless` : this.status.icon; @@ -63,7 +73,10 @@ export default { }; </script> <template> - <span :class="cssClass"> + <span + :class="[wrapperStyleClasses, { interactive: isInteractive }]" + :style="{ height: `${size}px`, width: `${size}px` }" + > <gl-icon :name="icon" :size="size" :class="cssClasses" :aria-label="status.icon" /> </span> </template> 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/assets/stylesheets/bootstrap_migration_components.scss b/app/assets/stylesheets/bootstrap_migration_components.scss index d4f0c96c613..676e69707c7 100644 --- a/app/assets/stylesheets/bootstrap_migration_components.scss +++ b/app/assets/stylesheets/bootstrap_migration_components.scss @@ -118,7 +118,14 @@ input[type='file'] { margin-bottom: 16px; .well-segment { - padding: 16px; + padding: 1rem; + + &.pipeline-info { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 0.25rem; + } &:not(:last-of-type) { border-bottom: 1px solid $well-inner-border; diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index 0aeb7208c59..ca0240b6a65 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -3,6 +3,17 @@ svg { fill: $green-500; } + + &.interactive { + &:hover { + background: $green-500; + + svg { + --svg-status-bg: #{$green-100}; + box-shadow: 0 0 0 1px $green-500; + } + } + } } .ci-status-icon-error, @@ -10,6 +21,17 @@ svg { fill: $red-500; } + + &.interactive { + &:hover { + background: $red-500; + + svg { + --svg-status-bg: #{$red-100}; + box-shadow: 0 0 0 1px $red-500; + } + } + } } .ci-status-icon-pending, @@ -19,11 +41,33 @@ svg { fill: $orange-500; } + + &.interactive { + &:hover { + background: $orange-500; + + svg { + --svg-status-bg: #{$orange-100}; + box-shadow: 0 0 0 1px $orange-500; + } + } + } } .ci-status-icon-running { svg { - fill: $blue-400; + fill: $blue-500; + } + + &.interactive { + &:hover { + background: $blue-500; + + svg { + --svg-status-bg: #{$blue-100}; + box-shadow: 0 0 0 1px $blue-500; + } + } } } @@ -32,7 +76,18 @@ .ci-status-icon-scheduled, .ci-status-icon-manual { svg { - fill: $gl-text-color; + fill: $gray-900; + } + + &.interactive { + &:hover { + background: $gray-900; + + svg { + --svg-status-bg: #{$gray-100}; + box-shadow: 0 0 0 1px $gray-900; + } + } } } @@ -42,7 +97,18 @@ .ci-status-icon-skipped, .ci-status-icon-notfound { svg { - fill: var(--gray-400, $gray-400); + fill: $gray-500; + } + + &.interactive { + &:hover { + background: $gray-500; + + svg { + --svg-status-bg: #{$gray-100}; + box-shadow: 0 0 0 1px $gray-500; + } + } } } diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss index b796f04750b..cfd215b81b8 100644 --- a/app/assets/stylesheets/framework/wells.scss +++ b/app/assets/stylesheets/framework/wells.scss @@ -39,7 +39,7 @@ .icon-container { display: inline-block; - margin-right: 8px; + margin: 0 0.5rem 0 0.25rem; svg { position: relative; diff --git a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss index 594800c84fd..3327f8da632 100644 --- a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss +++ b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss @@ -13,105 +13,6 @@ } } -@mixin mini-pipeline-graph-color( - $color-background-default, - $color-background-hover-focus, - $color-background-active, - $color-foreground-default, - $color-foreground-hover-focus, - $color-foreground-active -) { - background-color: $color-background-default; - border-color: $color-foreground-default; - - svg { - fill: $color-foreground-default; - } - - &:hover, - &:focus { - background-color: $color-background-hover-focus; - border-color: $color-foreground-hover-focus; - - svg { - fill: $color-foreground-hover-focus; - } - } - - &:active { - background-color: $color-background-active; - border-color: $color-foreground-active; - - svg { - fill: $color-foreground-active; - } - } - - &:focus { - box-shadow: 0 0 4px 1px $blue-300; - } -} - -@mixin mini-pipeline-item() { - border-radius: 100px; - background-color: var(--white, $white); - border-width: 1px; - border-style: solid; - width: $ci-action-icon-size; - height: $ci-action-icon-size; - margin: 0; - padding: 0; - position: relative; - vertical-align: middle; - - &:hover, - &:active, - &:focus { - outline: none; - border-width: 2px; - } - - // Dropdown button animation in mini pipeline graph - &.ci-status-icon-success { - @include mini-pipeline-graph-color(var(--white, $white), $green-100, $green-200, $green-500, $green-600, $green-700); - } - - &.ci-status-icon-failed { - @include mini-pipeline-graph-color(var(--white, $white), $red-100, $red-200, $red-500, $red-600, $red-700); - } - - &.ci-status-icon-pending, - &.ci-status-icon-waiting-for-resource, - &.ci-status-icon-success-with-warnings { - @include mini-pipeline-graph-color(var(--white, $white), $orange-50, $orange-100, $orange-500, $orange-600, $orange-700); - } - - &.ci-status-icon-running { - @include mini-pipeline-graph-color(var(--white, $white), $blue-100, $blue-200, $blue-500, $blue-600, $blue-700); - } - - &.ci-status-icon-canceled, - &.ci-status-icon-scheduled, - &.ci-status-icon-disabled, - &.ci-status-icon-manual { - @include mini-pipeline-graph-color( - var(--white, $white), - var(--gray-500, $gray-500), - var(--gray-700, $gray-700), - var(--gray-900, $gray-900), - var(--gray-950, $gray-950), - var(--black, $black) - ); - } - - &.ci-status-icon-preparing, - &.ci-status-icon-created, - &.ci-status-icon-not-found, - &.ci-status-icon-skipped { - @include mini-pipeline-graph-color(var(--white, $white), var(--gray-100, $gray-100), var(--gray-200, $gray-200), var(--gray-400, $gray-400), var(--gray-500, $gray-500), var(--gray-600, $gray-600)); - } -} - /** Action icons inside dropdowns: - mini graph in pipelines table diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss index bd5888af3ad..bef7ecc8a7e 100644 --- a/app/assets/stylesheets/page_bundles/merge_requests.scss +++ b/app/assets/stylesheets/page_bundles/merge_requests.scss @@ -121,10 +121,6 @@ $tabs-holder-z-index: 250; @include media-breakpoint-down(sm) { flex-direction: column; - .stage-cell .stage-container { - margin-top: 16px; - } - .dropdown .mini-pipeline-graph-dropdown-menu.dropdown-menu { transform: initial; } diff --git a/app/assets/stylesheets/page_bundles/pipelines.scss b/app/assets/stylesheets/page_bundles/pipelines.scss index 7b54be5c91f..a225a0f0061 100644 --- a/app/assets/stylesheets/page_bundles/pipelines.scss +++ b/app/assets/stylesheets/page_bundles/pipelines.scss @@ -73,36 +73,12 @@ // Mini Pipelines .stage-cell { - .mini-pipeline-graph-dropdown-toggle { - svg { - height: $ci-action-icon-size; - width: $ci-action-icon-size; - position: absolute; - top: -1px; - left: -1px; - z-index: 2; - overflow: visible; - } - - &:hover, - &:active, - &:focus { - svg { - top: -2px; - left: -2px; - } - } - } - .stage-container { - display: inline-block; - position: relative; - vertical-align: middle; - height: $ci-action-icon-size; - margin: 3px 0; + align-items: center; + display: inline-flex; + .stage-container { - margin-left: 6px; + margin-left: 4px; } // Hack to show a button tooltip inline @@ -118,44 +94,15 @@ &:not(:last-child) { &::after { content: ''; - width: 7px; + width: 4px; position: absolute; - right: -7px; - top: 11px; - border-bottom: 2px solid var(--border-color, $border-color); - } - } - - //delete when all pipelines are updated to new size - &.mr-widget-pipeline-stages { - + .stage-container { - margin-left: 4px; - } - - &:not(:last-child) { - &::after { - width: 4px; - right: -4px; - top: 11px; - } + right: -4px; + border-bottom: 2px solid $gray-200; } } } } -// Commit mini pipeline (HAML) -button.mini-pipeline-graph-dropdown-toggle, -// GlDropdown mini pipeline (Vue) -// As the `mini-pipeline-item` mixin specificity is lower -// than the toggle of dropdown with 'variant="link"' we add -// classes ".gl-button.btn-link" to make it more specific -// and avoid having the size overriden -// -// See https://gitlab.com/gitlab-org/gitlab/-/issues/320737 -button.gl-button.btn-link.mini-pipeline-graph-dropdown-toggle { - @include mini-pipeline-item(); -} - // Action icons inside dropdowns: // mini graph in pipelines table // mini graph in MR widget pipeline diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index cc8ea1493fc..afe57bb26e6 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -35,9 +35,6 @@ } .mr-widget-pipeline-graph { - display: inline-block; - vertical-align: middle; - .dropdown-menu { margin-top: 11px; } @@ -45,8 +42,6 @@ } .branch-info .commit-icon { - margin-right: 8px; - svg { top: 3px; } diff --git a/app/finders/error_tracking/errors_finder.rb b/app/finders/error_tracking/errors_finder.rb deleted file mode 100644 index c361d6e2fc2..00000000000 --- a/app/finders/error_tracking/errors_finder.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -module ErrorTracking - class ErrorsFinder - def initialize(current_user, project, params) - @current_user = current_user - @project = project - @params = params - end - - def execute - return ErrorTracking::Error.none unless authorized? - - collection = project.error_tracking_errors - collection = by_status(collection) - collection = sort(collection) - - collection.keyset_paginate(cursor: params[:cursor], per_page: limit) - end - - private - - attr_reader :current_user, :project, :params - - def by_status(collection) - if params[:status].present? && ErrorTracking::Error.statuses.key?(params[:status]) - collection.for_status(params[:status]) - else - collection - end - end - - def authorized? - Ability.allowed?(current_user, :read_sentry_issue, project) - end - - def sort(collection) - params[:sort] ? collection.sort_by_attribute(params[:sort]) : collection.order_id_desc - end - - def limit - # Restrict the maximum limit at 100 records. - [(params[:limit] || 20).to_i, 100].min - end - end -end diff --git a/app/graphql/types/ci/runner_status_enum.rb b/app/graphql/types/ci/runner_status_enum.rb index 2e65e2d4e1e..9ba680975f4 100644 --- a/app/graphql/types/ci/runner_status_enum.rb +++ b/app/graphql/types/ci/runner_status_enum.rb @@ -36,13 +36,8 @@ module Types description: "Runner that has not contacted this instance within the last #{::Ci::Runner::STALE_TIMEOUT.inspect}. Only available if legacyMode is null. Will be a possible return value starting in 15.0.", value: :stale - value 'NOT_CONNECTED', - description: 'Runner that has never contacted this instance.', - deprecated: { reason: "Use NEVER_CONTACTED instead. NEVER_CONTACTED will have a slightly different scope starting in 15.0, with STALE being returned instead after #{::Ci::Runner::STALE_TIMEOUT.inspect} of no contact", milestone: '14.6' }, - value: :not_connected - value 'NEVER_CONTACTED', - description: 'Runner that has never contacted this instance. Set legacyMode to null to utilize this value. Will replace NOT_CONNECTED starting in 15.0.', + description: 'Runner that has never contacted this instance.', value: :never_contacted end end diff --git a/app/graphql/types/work_item_type.rb b/app/graphql/types/work_item_type.rb index da76c43a377..cd784d54959 100644 --- a/app/graphql/types/work_item_type.rb +++ b/app/graphql/types/work_item_type.rb @@ -19,8 +19,7 @@ module Types field :title, GraphQL::Types::String, null: false, description: 'Title of the work item.' field :work_item_type, Types::WorkItems::TypeType, null: false, - description: 'Type assigned to the work item.', - method: :work_item_type_with_fallback # necessary until we validate the not null constraint + description: 'Type assigned to the work item.' markdown_field :title_html, null: true markdown_field :description_html, null: true diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb index b576b8802ae..6366ca0dfb1 100644 --- a/app/helpers/ci/runners_helper.rb +++ b/app/helpers/ci/runners_helper.rb @@ -17,7 +17,7 @@ module Ci title = s_("Runners|Runner is online; last contact was %{runner_contact} ago") % { runner_contact: time_ago_in_words(contacted_at) } icon = 'status-active' span_class = 'gl-text-green-500' - when :not_connected, :never_contacted + when :never_contacted title = s_("Runners|Runner has never contacted this instance") icon = 'warning-solid' when :offline diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5bb9f8fcfe1..11e2629e8dd 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -45,6 +45,7 @@ module Ci has_one :runtime_metadata, class_name: 'Ci::RunningBuild', foreign_key: :build_id has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id, inverse_of: :build has_many :report_results, class_name: 'Ci::BuildReportResult', inverse_of: :build + has_one :namespace, through: :project # Projects::DestroyService destroys Ci::Pipelines, which use_fast_destroy on :job_artifacts # before we delete builds. By doing this, the relation should be empty and not fire any diff --git a/app/models/ci/namespace_settings.rb b/app/models/ci/namespace_settings.rb new file mode 100644 index 00000000000..d519a48311f --- /dev/null +++ b/app/models/ci/namespace_settings.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# CI::NamespaceSettings mixin +# +# This module is intended to encapsulate CI/CD settings-specific logic +# and be prepended in the `Namespace` model +module Ci + module NamespaceSettings + # Overridden in EE::Namespace + def allow_stale_runner_pruning? + false + end + + # Overridden in EE::Namespace + def allow_stale_runner_pruning=(_value) + raise NotImplementedError + end + end +end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 5f59dbba6b2..53cf7c080ad 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -12,6 +12,7 @@ module Ci include Gitlab::Utils::StrongMemoize include TaggableQueries include Presentable + include EachBatch add_authentication_token_field :token, encrypted: :optional, expires_at: :compute_token_expiration, expiration_enforced?: :token_expiration_enforced? @@ -59,7 +60,7 @@ module Ci AVAILABLE_TYPES_LEGACY = %w[specific shared].freeze AVAILABLE_TYPES = runner_types.keys.freeze - AVAILABLE_STATUSES = %w[active paused online offline not_connected never_contacted stale].freeze # TODO: Remove in %15.0: not_connected. In %16.0: active, paused. Relevant issues: https://gitlab.com/gitlab-org/gitlab/-/issues/347303, https://gitlab.com/gitlab-org/gitlab/-/issues/347305, https://gitlab.com/gitlab-org/gitlab/-/issues/344648 + AVAILABLE_STATUSES = %w[active paused online offline never_contacted stale].freeze # TODO: Remove in %16.0: active, paused. Relevant issues: https://gitlab.com/gitlab-org/gitlab/-/issues/347303, https://gitlab.com/gitlab-org/gitlab/-/issues/344648 AVAILABLE_SCOPES = (AVAILABLE_TYPES_LEGACY + AVAILABLE_TYPES + AVAILABLE_STATUSES).freeze FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze @@ -83,7 +84,6 @@ module Ci scope :recent, -> { where('ci_runners.created_at >= :date OR ci_runners.contacted_at >= :date', date: stale_deadline) } scope :stale, -> { where('ci_runners.created_at < :date AND (ci_runners.contacted_at IS NULL OR ci_runners.contacted_at < :date)', date: stale_deadline) } scope :offline, -> { where(arel_table[:contacted_at].lteq(online_contact_time_deadline)) } - scope :not_connected, -> { where(contacted_at: nil) } # TODO: Remove in 15.0 scope :never_contacted, -> { where(contacted_at: nil) } scope :ordered, -> { order(id: :desc) } @@ -337,7 +337,7 @@ module Ci # TODO Remove in %16.0 in favor of `status` for REST calls, see https://gitlab.com/gitlab-org/gitlab/-/issues/344648 def deprecated_rest_status if contacted_at.nil? - :not_connected + :never_contacted elsif active? online? ? :online : :offline else diff --git a/app/models/issue.rb b/app/models/issue.rb index 5682ec08a76..d4eb77ef6de 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -186,8 +186,6 @@ class Issue < ApplicationRecord after_save :ensure_metrics, unless: :importing? after_create_commit :record_create_action, unless: :importing? - before_validation :ensure_work_item_type - attr_spammable :title, spam_title: true attr_spammable :description, spam_description: true @@ -611,20 +609,12 @@ class Issue < ApplicationRecord end # Necessary until all issues are backfilled and we add a NOT NULL constraint on the DB - def work_item_type_with_fallback - work_item_type || WorkItems::Type.default_by_type(issue_type) + def work_item_type + super || WorkItems::Type.default_by_type(issue_type) end private - # Issue create/update service provide a work item type - # adding this callback as there might be other mechanisms to create/update issues we are not handling - def ensure_work_item_type - return if work_item_type - - self.work_item_type = WorkItems::Type.default_by_type(issue_type) - end - override :persist_pg_full_text_search_vector def persist_pg_full_text_search_vector(search_vector) Issues::SearchData.upsert({ project_id: project_id, issue_id: id, search_vector: search_vector }, unique_by: %i(project_id issue_id)) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index f41bd6f5001..c9254aa432f 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -16,6 +16,7 @@ class Namespace < ApplicationRecord include Namespaces::Traversal::Linear include EachBatch include BlocksUnsafeSerialization + include Ci::NamespaceSettings # Temporary column used for back-filling project namespaces. # Remove it once the back-filling of all project namespaces is done. diff --git a/app/models/namespace_ci_cd_setting.rb b/app/models/namespace_ci_cd_setting.rb index 0e044f4fb10..c9c3a3206b6 100644 --- a/app/models/namespace_ci_cd_setting.rb +++ b/app/models/namespace_ci_cd_setting.rb @@ -5,3 +5,5 @@ class NamespaceCiCdSetting < ApplicationRecord # rubocop:disable Gitlab/Namespac self.primary_key = :namespace_id end + +NamespaceCiCdSetting.prepend_mod diff --git a/app/models/project.rb b/app/models/project.rb index dd5bc9ded6a..027b2257f2e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -412,7 +412,6 @@ class Project < ApplicationRecord has_one :operations_feature_flags_client, class_name: 'Operations::FeatureFlagsClient' has_many :operations_feature_flags_user_lists, class_name: 'Operations::FeatureFlags::UserList' - has_many :error_tracking_errors, inverse_of: :project, class_name: 'ErrorTracking::Error' has_many :error_tracking_client_keys, inverse_of: :project, class_name: 'ErrorTracking::ClientKey' has_many :timelogs diff --git a/app/services/error_tracking/base_service.rb b/app/services/error_tracking/base_service.rb index 598621f70e1..d2ecd0a6d5a 100644 --- a/app/services/error_tracking/base_service.rb +++ b/app/services/error_tracking/base_service.rb @@ -71,5 +71,15 @@ module ErrorTracking def can_update? can?(current_user, :update_sentry_issue, project) end + + def error_repository + Gitlab::ErrorTracking::ErrorRepository.build(project) + end + + def handle_error_repository_exceptions + yield + rescue Gitlab::ErrorTracking::ErrorRepository::DatabaseError => e + { error: e.message } + end end end diff --git a/app/services/error_tracking/collect_error_service.rb b/app/services/error_tracking/collect_error_service.rb index 6376b743255..8cb3793ba97 100644 --- a/app/services/error_tracking/collect_error_service.rb +++ b/app/services/error_tracking/collect_error_service.rb @@ -5,30 +5,24 @@ module ErrorTracking include Gitlab::Utils::StrongMemoize def execute - # Error is a way to group events based on common data like name or cause - # of exception. We need to keep a sane balance here between taking too little - # and too much data into group logic. - error = project.error_tracking_errors.report_error( - name: exception['type'], # Example: ActionView::MissingTemplate - description: exception['value'], # Example: Missing template posts/show in... - actor: actor, # Example: PostsController#show - platform: event['platform'], # Example: ruby - timestamp: timestamp - ) - - # The payload field contains all the data on error including stacktrace in jsonb. - # Together with occurred_at these are 2 main attributes that we need to save here. - error.events.create!( - environment: event['environment'], + error_repository.report_error( + name: exception['type'], description: exception['value'], - level: event['level'], + actor: actor, + platform: event['platform'], occurred_at: timestamp, + environment: event['environment'], + level: event['level'], payload: event ) end private + def error_repository + Gitlab::ErrorTracking::ErrorRepository.build(project) + end + def event @event ||= format_event(params[:event]) end diff --git a/app/services/error_tracking/issue_details_service.rb b/app/services/error_tracking/issue_details_service.rb index 1614c597a8e..e82ad540e57 100644 --- a/app/services/error_tracking/issue_details_service.rb +++ b/app/services/error_tracking/issue_details_service.rb @@ -49,13 +49,10 @@ module ErrorTracking # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596 # if project_error_tracking_setting.integrated_client? - error = project.error_tracking_errors.find(issue_id) - - # We use the same response format as project_error_tracking_setting - # method below for compatibility with existing code. - { - issue: error.to_sentry_detailed_error - } + handle_error_repository_exceptions do + error = error_repository.find_error(issue_id) + { issue: error } + end else project_error_tracking_setting.issue_details(issue_id: issue_id) end diff --git a/app/services/error_tracking/issue_latest_event_service.rb b/app/services/error_tracking/issue_latest_event_service.rb index 1bf86c658fc..0290c8eac86 100644 --- a/app/services/error_tracking/issue_latest_event_service.rb +++ b/app/services/error_tracking/issue_latest_event_service.rb @@ -26,14 +26,13 @@ module ErrorTracking # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596 # if project_error_tracking_setting.integrated_client? - error = project.error_tracking_errors.find(issue_id) - event = error.events.last + handle_error_repository_exceptions do + event = error_repository.last_event_for(issue_id) - # We use the same response format as project_error_tracking_setting - # method below for compatibility with existing code. - { - latest_event: event.to_sentry_error_event - } + # We use the same response format as project_error_tracking_setting + # method below for compatibility with existing code. + { latest_event: event } + end else project_error_tracking_setting.issue_latest_event(issue_id: issue_id) end diff --git a/app/services/error_tracking/issue_update_service.rb b/app/services/error_tracking/issue_update_service.rb index 624e5f94dde..ca5e8d656a6 100644 --- a/app/services/error_tracking/issue_update_service.rb +++ b/app/services/error_tracking/issue_update_service.rb @@ -84,14 +84,12 @@ module ErrorTracking # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596 # if project_error_tracking_setting.integrated_client? - error = project.error_tracking_errors.find(opts[:issue_id]) - error.status = opts[:params][:status] - error.save! + updated = error_repository.update_error(opts[:issue_id], status: opts[:params][:status]) # We use the same response format as project_error_tracking_setting # method below for compatibility with existing code. { - updated: true + updated: updated } else project_error_tracking_setting.update_issue(**opts) diff --git a/app/services/error_tracking/list_issues_service.rb b/app/services/error_tracking/list_issues_service.rb index 1979816b88d..ca7208dba96 100644 --- a/app/services/error_tracking/list_issues_service.rb +++ b/app/services/error_tracking/list_issues_service.rb @@ -73,24 +73,24 @@ module ErrorTracking if project_error_tracking_setting.integrated_client? # We are going to support more options in the future. # For now we implement the bare minimum for rendering the list in UI. - filter_opts = { - status: opts[:issue_status], + list_opts = { + filters: { status: opts[:issue_status] }, sort: opts[:sort], limit: opts[:limit], cursor: opts[:cursor] } - errors = ErrorTracking::ErrorsFinder.new(current_user, project, filter_opts).execute + errors, pagination = error_repository.list_errors(**list_opts) - pagination = {} - pagination[:next] = { cursor: errors.cursor_for_next_page } if errors.has_next_page? - pagination[:previous] = { cursor: errors.cursor_for_previous_page } if errors.has_previous_page? + pagination_hash = {} + pagination_hash[:next] = { cursor: pagination.next } if pagination.next + pagination_hash[:previous] = { cursor: pagination.prev } if pagination.prev # We use the same response format as project_error_tracking_setting # method below for compatibility with existing code. { - issues: errors.map(&:to_sentry_error), - pagination: pagination + issues: errors, + pagination: pagination_hash } else project_error_tracking_setting.list_sentry_issues(**opts) diff --git a/app/views/projects/_merge_request_merge_method_settings.html.haml b/app/views/projects/_merge_request_merge_method_settings.html.haml index 250f7e94e84..cb660750632 100644 --- a/app/views/projects/_merge_request_merge_method_settings.html.haml +++ b/app/views/projects/_merge_request_merge_method_settings.html.haml @@ -4,7 +4,7 @@ %b= s_('ProjectSettings|Merge method') %p.text-secondary = s_('ProjectSettings|Determine what happens to the commit history when you merge a merge request.') - = link_to s_('ProjectSettings|Learn about commit history.'), help_page_path('user/project/merge_requests/commits.md'), target: '_blank', rel: 'noopener noreferrer' + = link_to s_('ProjectSettings|How do they differ?'), help_page_path('user/project/merge_requests/methods/index.md'), target: '_blank', rel: 'noopener noreferrer' .form-check.mb-2 = form.radio_button :merge_method, :merge, class: "js-merge-method-radio form-check-input" = label_tag :project_merge_method_merge, class: 'form-check-label' do |