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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_managers_table.vue12
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_manager_shared.fragment.graphql1
-rw-r--r--app/assets/javascripts/issues/list/components/issue_card_time_info.vue31
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue4
-rw-r--r--app/assets/javascripts/service_desk/components/service_desk_list_app.vue47
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue15
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js10
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js35
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue8
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue16
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/store/getters.js13
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/utils.js13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js4
-rw-r--r--app/assets/javascripts/work_items/list/components/work_items_list_app.vue13
-rw-r--r--app/assets/javascripts/work_items/list/index.js7
-rw-r--r--app/assets/javascripts/work_items/list/queries/base_work_item_widgets.fragment.graphql38
-rw-r--r--app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql27
-rw-r--r--app/assets/javascripts/work_items/list/queries/work_item_widgets.fragment.graphql5
-rw-r--r--app/assets/javascripts/work_items/utils.js18
-rw-r--r--app/controllers/projects/alerting/notifications_controller.rb6
-rw-r--r--app/controllers/projects/prometheus/alerts_controller.rb43
-rw-r--r--app/graphql/mutations/work_items/linked_items/add.rb3
-rw-r--r--app/graphql/mutations/work_items/linked_items/base.rb7
-rw-r--r--app/graphql/mutations/work_items/linked_items/remove.rb28
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/helpers/work_items_helper.rb6
-rw-r--r--app/models/ci/build.rb7
-rw-r--r--app/models/concerns/linkable_item.rb1
-rw-r--r--app/models/project.rb9
-rw-r--r--app/presenters/ci/build_runner_presenter.rb10
-rw-r--r--app/services/ci/register_job_service.rb16
-rw-r--r--app/services/work_items/related_work_item_links/destroy_service.rb85
-rw-r--r--app/views/groups/work_items/index.html.haml2
-rw-r--r--app/views/projects/merge_requests/_code_dropdown.html.haml8
-rw-r--r--app/views/projects/merge_requests/_page.html.haml6
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/tabs/_tab.html.haml4
38 files changed, 440 insertions, 123 deletions
diff --git a/app/assets/javascripts/ci/runner/components/runner_managers_table.vue b/app/assets/javascripts/ci/runner/components/runner_managers_table.vue
index 10790c398b0..58244e1f2df 100644
--- a/app/assets/javascripts/ci/runner/components/runner_managers_table.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_managers_table.vue
@@ -6,6 +6,7 @@ import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { tableField } from '../utils';
import { I18N_STATUS_NEVER_CONTACTED } from '../constants';
import RunnerStatusBadge from './runner_status_badge.vue';
+import RunnerJobStatusBadge from './runner_job_status_badge.vue';
export default {
name: 'RunnerManagersTable',
@@ -15,6 +16,7 @@ export default {
HelpPopover,
GlIntersperse,
RunnerStatusBadge,
+ RunnerJobStatusBadge,
RunnerUpgradeStatusIcon: () =>
import('ee_component/ci/runner/components/runner_upgrade_status_icon.vue'),
},
@@ -52,7 +54,15 @@ export default {
</help-popover>
</template>
<template #cell(status)="{ item = {} }">
- <runner-status-badge :contacted-at="item.contactedAt" :status="item.status" />
+ <runner-status-badge
+ class="gl-vertical-align-middle"
+ :contacted-at="item.contactedAt"
+ :status="item.status"
+ />
+ <runner-job-status-badge
+ class="gl-vertical-align-middle"
+ :job-status="item.jobExecutionStatus"
+ />
</template>
<template #cell(version)="{ item = {} }">
{{ item.version }}
diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_manager_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_manager_shared.fragment.graphql
index ead005d1252..84d32e24f24 100644
--- a/app/assets/javascripts/ci/runner/graphql/show/runner_manager_shared.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/show/runner_manager_shared.fragment.graphql
@@ -9,4 +9,5 @@ fragment CiRunnerManagerShared on CiRunnerManager {
platformName
ipAddress
contactedAt
+ jobExecutionStatus
}
diff --git a/app/assets/javascripts/issues/list/components/issue_card_time_info.vue b/app/assets/javascripts/issues/list/components/issue_card_time_info.vue
index dde1a4fd2d6..06c2e29a904 100644
--- a/app/assets/javascripts/issues/list/components/issue_card_time_info.vue
+++ b/app/assets/javascripts/issues/list/components/issue_card_time_info.vue
@@ -10,6 +10,8 @@ import {
newDateAsLocaleTime,
} from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
+import { STATE_CLOSED } from '~/work_items/constants';
+import { isMilestoneWidget, isStartAndDueDateWidget } from '~/work_items/utils';
export default {
components: {
@@ -26,9 +28,12 @@ export default {
},
},
computed: {
+ milestone() {
+ return this.issue.milestone || this.issue.widgets?.find(isMilestoneWidget)?.milestone;
+ },
milestoneDate() {
- if (this.issue.milestone?.dueDate) {
- const { dueDate, startDate } = this.issue.milestone;
+ if (this.milestone.dueDate) {
+ const { dueDate, startDate } = this.milestone;
const date = dateInWords(newDateAsLocaleTime(dueDate), true);
const remainingTime = this.milestoneRemainingTime(dueDate, startDate);
return `${date} (${remainingTime})`;
@@ -36,15 +41,19 @@ export default {
return __('Milestone');
},
milestoneLink() {
- return this.issue.milestone.webPath || this.issue.milestone.webUrl;
+ return this.milestone.webPath || this.milestone.webUrl;
},
dueDate() {
- return this.issue.dueDate && dateInWords(newDateAsLocaleTime(this.issue.dueDate), true);
+ return this.issue.dueDate || this.issue.widgets?.find(isStartAndDueDateWidget)?.dueDate;
+ },
+ dueDateText() {
+ return this.dueDate && dateInWords(newDateAsLocaleTime(this.dueDate), true);
+ },
+ isClosed() {
+ return this.issue.state === STATUS_CLOSED || this.issue.state === STATE_CLOSED;
},
showDueDateInRed() {
- return (
- isInPast(newDateAsLocaleTime(this.issue.dueDate)) && this.issue.state !== STATUS_CLOSED
- );
+ return isInPast(newDateAsLocaleTime(this.dueDate)) && !this.isClosed;
},
timeEstimate() {
return this.issue.humanTimeEstimate || this.issue.timeStats?.humanTimeEstimate;
@@ -73,7 +82,7 @@ export default {
<template>
<span>
<span
- v-if="issue.milestone"
+ v-if="milestone"
class="issuable-milestone gl-mr-3 gl-text-truncate gl-max-w-26 gl-display-inline-block gl-vertical-align-bottom"
data-testid="issuable-milestone"
>
@@ -84,11 +93,11 @@ export default {
class="gl-font-sm gl-text-gray-500!"
>
<gl-icon name="clock" :size="12" />
- {{ issue.milestone.title }}
+ {{ milestone.title }}
</gl-link>
</span>
<span
- v-if="issue.dueDate"
+ v-if="dueDate"
v-gl-tooltip
class="issuable-due-date gl-mr-3"
:class="{ 'gl-text-red-500': showDueDateInRed }"
@@ -96,7 +105,7 @@ export default {
data-testid="issuable-due-date"
>
<gl-icon name="calendar" :size="12" />
- {{ dueDate }}
+ {{ dueDateText }}
</span>
<span
v-if="timeEstimate"
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index c839d7a53cd..4f0907df7f2 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -260,7 +260,9 @@ export default {
</gl-intersection-observer>
</td>
<td class="tree-time-ago text-right cursor-default gl-text-secondary">
- <timeago-tooltip v-if="commitData" :time="commitData.committedDate" />
+ <gl-intersection-observer @appear="rowAppeared" @disappear="rowDisappeared">
+ <timeago-tooltip v-if="commitData" :time="commitData.committedDate" />
+ </gl-intersection-observer>
<gl-skeleton-loader v-if="showSkeletonLoader" :lines="1" />
</td>
</tr>
diff --git a/app/assets/javascripts/service_desk/components/service_desk_list_app.vue b/app/assets/javascripts/service_desk/components/service_desk_list_app.vue
index 3bba237c9d9..a831fb12d5e 100644
--- a/app/assets/javascripts/service_desk/components/service_desk_list_app.vue
+++ b/app/assets/javascripts/service_desk/components/service_desk_list_app.vue
@@ -6,6 +6,7 @@ import { fetchPolicies } from '~/lib/graphql';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import axios from '~/lib/utils/axios_utils';
import { getParameterByName, joinPaths } from '~/lib/utils/url_utility';
+import { scrollUp } from '~/lib/utils/scroll_utils';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { DEFAULT_PAGE_SIZE, issuableListTabs } from '~/vue_shared/issuable/list/constants';
@@ -206,6 +207,18 @@ export default {
[STATUS_ALL]: allIssues?.count,
};
},
+ currentTabCount() {
+ return this.tabCounts[this.state] ?? 0;
+ },
+ showPaginationControls() {
+ return (
+ this.serviceDeskIssues.length > 0 &&
+ (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage)
+ );
+ },
+ showPageSizeControls() {
+ return this.currentTabCount > DEFAULT_PAGE_SIZE;
+ },
isLoading() {
return this.$apollo.queries.serviceDeskIssues.loading;
},
@@ -404,6 +417,32 @@ export default {
this.$router.push({ query: this.urlParams });
},
+ handleNextPage() {
+ this.pageParams = {
+ afterCursor: this.pageInfo.endCursor,
+ firstPageSize: this.pageSize,
+ };
+ scrollUp();
+
+ this.$router.push({ query: this.urlParams });
+ },
+ handlePreviousPage() {
+ this.pageParams = {
+ beforeCursor: this.pageInfo.startCursor,
+ lastPageSize: this.pageSize,
+ };
+ scrollUp();
+
+ this.$router.push({ query: this.urlParams });
+ },
+ handlePageSizeChange(newPageSize) {
+ const pageParam = getParameterByName(PARAM_LAST_PAGE_SIZE) ? 'lastPageSize' : 'firstPageSize';
+ this.pageParams[pageParam] = newPageSize;
+ this.pageSize = newPageSize;
+ scrollUp();
+
+ this.$router.push({ query: this.urlParams });
+ },
handleSort(sortKey) {
if (this.sortKey === sortKey) {
return;
@@ -525,6 +564,8 @@ export default {
:issuables-loading="isLoading"
:initial-filter-value="filterTokens"
:show-filtered-search-friendly-text="hasOrFeature"
+ :show-pagination-controls="showPaginationControls"
+ :show-page-size-change-controls="showPageSizeControls"
:sort-options="sortOptions"
:initial-sort-by="sortKey"
:is-manual-ordering="isManualOrdering"
@@ -533,11 +574,17 @@ export default {
:tab-counts="tabCounts"
:current-tab="state"
:default-page-size="pageSize"
+ :has-next-page="pageInfo.hasNextPage"
+ :has-previous-page="pageInfo.hasPreviousPage"
sync-filter-and-sort
+ use-keyset-pagination
@click-tab="handleClickTab"
@filter="handleFilter"
@sort="handleSort"
@reorder="handleReorder"
+ @next-page="handleNextPage"
+ @previous-page="handlePreviousPage"
+ @page-size-change="handlePageSizeChange"
>
<template #empty-state>
<empty-state-with-any-issues :has-search="hasSearch" :is-open-tab="isOpenTab" />
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
index bd79962f1a1..b85b163cea9 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
@@ -5,6 +5,7 @@ import { GlDisclosureDropdownGroup, GlLoadingIcon } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import Tracking from '~/tracking';
import { getFormattedItem } from '../utils';
import {
@@ -18,6 +19,8 @@ import {
PATH_GROUP_TITLE,
GROUP_TITLES,
MAX_ROWS,
+ TRACKING_ACTIVATE_COMMAND_PALETTE,
+ TRACKING_HANDLE_LABEL_MAP,
} from './constants';
import SearchItem from './search_item.vue';
import { commandMapper, linksReducer, autocompleteQuery, fileMapper } from './utils';
@@ -29,6 +32,7 @@ export default {
GlLoadingIcon,
SearchItem,
},
+ mixins: [Tracking.mixin()],
inject: [
'commandPaletteCommands',
'commandPaletteLinks',
@@ -134,10 +138,15 @@ export default {
immediate: true,
},
handle: {
- handler() {
- this.debouncedSearch();
+ handler(value, oldValue) {
+ // Do not run search immediately on component creation
+ if (oldValue !== undefined) this.debouncedSearch();
+
+ // Track immediately on component creation
+ const label = TRACKING_HANDLE_LABEL_MAP[value] ?? 'unknown';
+ this.track(TRACKING_ACTIVATE_COMMAND_PALETTE, { label });
},
- immediate: false,
+ immediate: true,
},
},
updated() {
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
index a43e621da44..f6f4e36e43a 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
@@ -6,6 +6,16 @@ export const PROJECT_HANDLE = ':';
export const ISSUE_HANDLE = '#';
export const PATH_HANDLE = '/';
+export const TRACKING_ACTIVATE_COMMAND_PALETTE = 'activate_command_palette';
+export const TRACKING_CLICK_COMMAND_PALETTE_ITEM = 'click_command_palette_item';
+export const TRACKING_HANDLE_LABEL_MAP = {
+ [COMMAND_HANDLE]: 'command',
+ [USER_HANDLE]: 'user',
+ [PROJECT_HANDLE]: 'project',
+ [PATH_HANDLE]: 'path',
+ // No ISSUE_HANDLE. See https://gitlab.com/gitlab-org/gitlab/-/issues/417434.
+};
+
export const COMMON_HANDLES = [COMMAND_HANDLE, USER_HANDLE, PROJECT_HANDLE];
export const SEARCH_OR_COMMAND_MODE_PLACEHOLDER = sprintf(
s__(
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js
index 347a8ffb0b4..32abbbfd3c2 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js
@@ -1,6 +1,11 @@
import { isNil, omitBy } from 'lodash';
import { objectToQuery, joinPaths } from '~/lib/utils/url_utility';
-import { SEARCH_SCOPE, GLOBAL_COMMANDS_GROUP_TITLE } from './constants';
+import { TRACKING_UNKNOWN_ID } from '~/super_sidebar/constants';
+import {
+ SEARCH_SCOPE,
+ GLOBAL_COMMANDS_GROUP_TITLE,
+ TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+} from './constants';
export const commandMapper = ({ name, items }) => {
// TODO: we filter out invite_members for now, because it is complicated to add the invite members modal here
@@ -12,18 +17,34 @@ export const commandMapper = ({ name, items }) => {
};
export const linksReducer = (acc, menuItem) => {
+ const trackingAttrs = ({ id, title }) => {
+ return {
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': id || TRACKING_UNKNOWN_ID,
+ ...(id
+ ? {}
+ : {
+ 'data-track-extra': JSON.stringify({ title }),
+ }),
+ },
+ };
+ };
+
acc.push({
text: menuItem.title,
keywords: menuItem.title,
icon: menuItem.icon,
href: menuItem.link,
+ ...trackingAttrs(menuItem),
});
if (menuItem.items?.length) {
- const items = menuItem.items.map(({ title, link }) => ({
- keywords: title,
- text: [menuItem.title, title].join(' > '),
- href: link,
+ const items = menuItem.items.map((item) => ({
+ keywords: item.title,
+ text: [menuItem.title, item.title].join(' > '),
+ href: item.link,
icon: menuItem.icon,
+ ...trackingAttrs(item),
}));
/* eslint-disable-next-line no-param-reassign */
@@ -37,6 +58,10 @@ export const fileMapper = (projectBlobPath, file) => {
icon: 'doc-code',
text: file,
href: joinPaths(projectBlobPath, file),
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': 'file',
+ },
};
};
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue
index 382d844ceee..ddadd6856ca 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue
@@ -2,6 +2,8 @@
import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, GlIcon } from '@gitlab/ui';
import { truncateNamespace } from '~/lib/utils/text_utility';
import { getItemsFromLocalStorage, removeItemFromLocalStorage } from '~/super_sidebar/utils';
+import { TRACKING_UNKNOWN_PANEL } from '~/super_sidebar/constants';
+import { TRACKING_CLICK_COMMAND_PALETTE_ITEM } from '../command_palette/constants';
import FrequentItem from './frequent_item.vue';
export default {
@@ -65,6 +67,12 @@ export default {
// validator, and the href field ensures it renders a link.
text: item.name,
href: item.webUrl,
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': item.id,
+ 'data-track-property': TRACKING_UNKNOWN_PANEL,
+ 'data-track-extra': JSON.stringify({ title: item.name }),
+ },
},
forRenderer: {
id: item.id,
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue
index 9a375837102..295927149d9 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue
@@ -1,6 +1,8 @@
<script>
import { GlDisclosureDropdownGroup } from '@gitlab/ui';
import { PLACES } from '~/vue_shared/global_search/constants';
+import { TRACKING_UNKNOWN_ID, TRACKING_UNKNOWN_PANEL } from '~/super_sidebar/constants';
+import { TRACKING_CLICK_COMMAND_PALETTE_ITEM } from '../command_palette/constants';
export default {
name: 'DefaultPlaces',
@@ -18,7 +20,19 @@ export default {
group() {
return {
name: this.$options.i18n.PLACES,
- items: this.contextSwitcherLinks.map(({ title, link }) => ({ text: title, href: link })),
+ items: this.contextSwitcherLinks.map(({ title, link }) => ({
+ text: title,
+ href: link,
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ // The label and property are hard-coded as unknown for now for
+ // parity with the existing corresponding context switcher items.
+ // Once the context switcher is removed, these can be changed.
+ 'data-track-label': TRACKING_UNKNOWN_ID,
+ 'data-track-property': TRACKING_UNKNOWN_PANEL,
+ 'data-track-extra': JSON.stringify({ title }),
+ },
+ })),
};
},
},
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js b/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js
index 6871dabc9a1..79be56f1427 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js
@@ -14,6 +14,7 @@ import {
SEARCH_RESULTS_ORDER,
} from '~/vue_shared/global_search/constants';
import { getFormattedItem } from '../utils';
+import { TRACKING_CLICK_COMMAND_PALETTE_ITEM } from '../command_palette/constants';
import {
ICON_GROUP,
@@ -172,6 +173,10 @@ export const scopedSearchOptions = (state, getters) => {
scopeCategory: PROJECTS_CATEGORY,
icon: ICON_PROJECT,
href: getters.projectUrl,
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': 'scoped_in_project',
+ },
});
}
@@ -182,6 +187,10 @@ export const scopedSearchOptions = (state, getters) => {
scopeCategory: GROUPS_CATEGORY,
icon: state.searchContext.group?.full_name?.includes('/') ? ICON_SUBGROUP : ICON_GROUP,
href: getters.groupUrl,
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': 'scoped_in_group',
+ },
});
}
@@ -189,6 +198,10 @@ export const scopedSearchOptions = (state, getters) => {
text: 'scoped-in-all',
description: MSG_IN_ALL_GITLAB,
href: getters.allUrl,
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': 'scoped_in_all',
+ },
});
return items;
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/utils.js b/app/assets/javascripts/super_sidebar/components/global_search/utils.js
index 11d1fa1ab95..2c369cbdf5f 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/utils.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/utils.js
@@ -1,5 +1,5 @@
import { pickBy } from 'lodash';
-import { truncateNamespace } from '~/lib/utils/text_utility';
+import { slugify, truncateNamespace } from '~/lib/utils/text_utility';
import {
GROUPS_CATEGORY,
PROJECTS_CATEGORY,
@@ -7,6 +7,7 @@ import {
ISSUES_CATEGORY,
RECENT_EPICS_CATEGORY,
} from '~/vue_shared/global_search/constants';
+import { TRACKING_CLICK_COMMAND_PALETTE_ITEM } from './command_palette/constants';
import { LARGE_AVATAR_PX, SMALL_AVATAR_PX } from './constants';
const getTruncatedNamespace = (string) => {
@@ -61,6 +62,15 @@ export const getFormattedItem = (item, searchContext) => {
const avatarSize = getAvatarSize(category);
const entityId = getEntityId(item, searchContext);
const entityName = getEntityName(item, searchContext);
+ const trackingLabel = slugify(category ?? '');
+ const trackingAttrs = trackingLabel
+ ? {
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': slugify(category, '_'),
+ },
+ }
+ : {};
return pickBy(
{
@@ -75,6 +85,7 @@ export const getFormattedItem = (item, searchContext) => {
namespace,
entity_id: entityId,
entity_name: entityName,
+ ...trackingAttrs,
},
(val) => val !== undefined,
);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
index 4f8f8d6cb58..b6bcc68e5e0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
@@ -8,8 +8,10 @@ import {
function simplifyWidgetName(componentName) {
const noWidget = componentName.replace(/^Widget/, '');
+ const camelName = noWidget.charAt(0).toLowerCase() + noWidget.slice(1);
+ const tierlessName = camelName.replace(/(CE|EE)$/, '');
- return noWidget.charAt(0).toLowerCase() + noWidget.slice(1);
+ return tierlessName;
}
function baseRedisEventName(extensionName) {
diff --git a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
index fe7cb719bbb..026c48cf017 100644
--- a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
+++ b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
@@ -1,5 +1,7 @@
<script>
import * as Sentry from '@sentry/browser';
+import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
+import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import { STATUS_OPEN } from '~/issues/constants';
import { __, s__ } from '~/locale';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
@@ -14,6 +16,8 @@ export default {
issuableListTabs,
components: {
IssuableList,
+ IssueCardStatistics,
+ IssueCardTimeInfo,
},
inject: ['fullPath'],
data() {
@@ -57,6 +61,7 @@ export default {
:current-tab="state"
:error="error"
:issuables="workItems"
+ :issuables-loading="$apollo.queries.workItems.loading"
namespace="work-items"
recent-searches-storage-key="issues"
:search-input-placeholder="$options.i18n.searchPlaceholder"
@@ -66,8 +71,16 @@ export default {
:tabs="$options.issuableListTabs"
@dismiss-alert="error = undefined"
>
+ <template #timeframe="{ issuable = {} }">
+ <issue-card-time-info :issue="issuable" />
+ </template>
+
<template #status="{ issuable }">
{{ getStatus(issuable) }}
</template>
+
+ <template #statistics="{ issuable = {} }">
+ <issue-card-statistics :issue="issuable" />
+ </template>
</issuable-list>
</template>
diff --git a/app/assets/javascripts/work_items/list/index.js b/app/assets/javascripts/work_items/list/index.js
index 5cd38600779..113a3918e51 100644
--- a/app/assets/javascripts/work_items/list/index.js
+++ b/app/assets/javascripts/work_items/list/index.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
+import { parseBoolean } from '~/lib/utils/common_utils';
import WorkItemsListApp from './components/work_items_list_app.vue';
export const mountWorkItemsListApp = () => {
@@ -12,6 +13,8 @@ export const mountWorkItemsListApp = () => {
Vue.use(VueApollo);
+ const { fullPath, hasIssuableHealthStatusFeature, hasIssueWeightsFeature } = el.dataset;
+
return new Vue({
el,
name: 'WorkItemsListRoot',
@@ -19,7 +22,9 @@ export const mountWorkItemsListApp = () => {
defaultClient: createDefaultClient(),
}),
provide: {
- fullPath: el.dataset.fullPath,
+ fullPath,
+ hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
+ hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
},
render: (createComponent) => createComponent(WorkItemsListApp),
});
diff --git a/app/assets/javascripts/work_items/list/queries/base_work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/list/queries/base_work_item_widgets.fragment.graphql
new file mode 100644
index 00000000000..1198973d184
--- /dev/null
+++ b/app/assets/javascripts/work_items/list/queries/base_work_item_widgets.fragment.graphql
@@ -0,0 +1,38 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+fragment BaseWorkItemWidgets on WorkItemWidget {
+ ... on WorkItemWidgetAssignees {
+ type
+ assignees {
+ nodes {
+ ...User
+ }
+ }
+ }
+ ... on WorkItemWidgetLabels {
+ type
+ allowsScopedLabels
+ labels {
+ nodes {
+ id
+ color
+ description
+ title
+ }
+ }
+ }
+ ... on WorkItemWidgetMilestone {
+ type
+ milestone {
+ id
+ dueDate
+ startDate
+ title
+ webPath
+ }
+ }
+ ... on WorkItemWidgetStartAndDueDate {
+ type
+ dueDate
+ }
+}
diff --git a/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql b/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql
index 7ada2cf12dd..623527302f1 100644
--- a/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql
+++ b/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql
@@ -1,3 +1,5 @@
+#import "ee_else_ce/work_items/list/queries/work_item_widgets.fragment.graphql"
+
query getWorkItems($fullPath: ID!) {
group(fullPath: $fullPath) {
id
@@ -21,30 +23,7 @@ query getWorkItems($fullPath: ID!) {
updatedAt
webUrl
widgets {
- ... on WorkItemWidgetAssignees {
- assignees {
- nodes {
- id
- avatarUrl
- name
- username
- webUrl
- }
- }
- type
- }
- ... on WorkItemWidgetLabels {
- allowsScopedLabels
- labels {
- nodes {
- id
- color
- description
- title
- }
- }
- type
- }
+ ...WorkItemWidgets
}
workItemType {
id
diff --git a/app/assets/javascripts/work_items/list/queries/work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/list/queries/work_item_widgets.fragment.graphql
new file mode 100644
index 00000000000..6862df5d330
--- /dev/null
+++ b/app/assets/javascripts/work_items/list/queries/work_item_widgets.fragment.graphql
@@ -0,0 +1,5 @@
+#import "./base_work_item_widgets.fragment.graphql"
+
+fragment WorkItemWidgets on WorkItemWidget {
+ ...BaseWorkItemWidgets
+}
diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js
index 5a882977bc2..ac5d8b32fad 100644
--- a/app/assets/javascripts/work_items/utils.js
+++ b/app/assets/javascripts/work_items/utils.js
@@ -1,9 +1,25 @@
-import { WIDGET_TYPE_ASSIGNEES, WIDGET_TYPE_HIERARCHY, WIDGET_TYPE_LABELS } from './constants';
+import {
+ WIDGET_TYPE_ASSIGNEES,
+ WIDGET_TYPE_HEALTH_STATUS,
+ WIDGET_TYPE_HIERARCHY,
+ WIDGET_TYPE_LABELS,
+ WIDGET_TYPE_MILESTONE,
+ WIDGET_TYPE_START_AND_DUE_DATE,
+ WIDGET_TYPE_WEIGHT,
+} from './constants';
export const isAssigneesWidget = (widget) => widget.type === WIDGET_TYPE_ASSIGNEES;
+export const isHealthStatusWidget = (widget) => widget.type === WIDGET_TYPE_HEALTH_STATUS;
+
export const isLabelsWidget = (widget) => widget.type === WIDGET_TYPE_LABELS;
+export const isMilestoneWidget = (widget) => widget.type === WIDGET_TYPE_MILESTONE;
+
+export const isStartAndDueDateWidget = (widget) => widget.type === WIDGET_TYPE_START_AND_DUE_DATE;
+
+export const isWeightWidget = (widget) => widget.type === WIDGET_TYPE_WEIGHT;
+
export const findHierarchyWidgets = (widgets) =>
widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY);
diff --git a/app/controllers/projects/alerting/notifications_controller.rb b/app/controllers/projects/alerting/notifications_controller.rb
index 281ac14d3ce..b596cd74b03 100644
--- a/app/controllers/projects/alerting/notifications_controller.rb
+++ b/app/controllers/projects/alerting/notifications_controller.rb
@@ -66,15 +66,11 @@ module Projects
def integration
AlertManagement::HttpIntegrationsFinder.new(
project,
- endpoint_identifier: endpoint_identifier,
+ endpoint_identifier: params[:endpoint_identifier],
active: true
).execute.first
end
- def endpoint_identifier
- params[:endpoint_identifier] || AlertManagement::HttpIntegration::LEGACY_IDENTIFIERS
- end
-
def notification_payload
@notification_payload ||= params.permit![:notification]
end
diff --git a/app/controllers/projects/prometheus/alerts_controller.rb b/app/controllers/projects/prometheus/alerts_controller.rb
deleted file mode 100644
index 80a8dbf4729..00000000000
--- a/app/controllers/projects/prometheus/alerts_controller.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module Prometheus
- class AlertsController < Projects::ApplicationController
- respond_to :json
-
- protect_from_forgery except: [:notify]
-
- skip_before_action :project, only: [:notify]
-
- prepend_before_action :repository, :project_without_auth, only: [:notify]
-
- before_action :authorize_read_prometheus_alerts!, except: [:notify]
-
- feature_category :incident_management
- urgency :low
-
- def notify
- token = extract_alert_manager_token(request)
- result = notify_service.execute(token)
-
- head result.http_status
- end
-
- private
-
- def notify_service
- Projects::Prometheus::Alerts::NotifyService
- .new(project, params.permit!)
- end
-
- def extract_alert_manager_token(request)
- Doorkeeper::OAuth::Token.from_bearer_authorization(request)
- end
-
- def project_without_auth
- @project ||= Project
- .find_by_full_path("#{params[:namespace_id]}/#{params[:project_id]}")
- end
- end
- end
-end
diff --git a/app/graphql/mutations/work_items/linked_items/add.rb b/app/graphql/mutations/work_items/linked_items/add.rb
index b346b074e85..e0c17a61205 100644
--- a/app/graphql/mutations/work_items/linked_items/add.rb
+++ b/app/graphql/mutations/work_items/linked_items/add.rb
@@ -9,6 +9,9 @@ module Mutations
argument :link_type, ::Types::WorkItems::RelatedLinkTypeEnum,
required: false, description: 'Type of link. Defaults to `RELATED`.'
+ argument :work_items_ids, [::Types::GlobalIDType[::WorkItem]],
+ required: true,
+ description: "Global IDs of the items to link. Maximum number of IDs you can provide: #{MAX_WORK_ITEMS}."
private
diff --git a/app/graphql/mutations/work_items/linked_items/base.rb b/app/graphql/mutations/work_items/linked_items/base.rb
index 1d8d74b02ac..a1d9bced930 100644
--- a/app/graphql/mutations/work_items/linked_items/base.rb
+++ b/app/graphql/mutations/work_items/linked_items/base.rb
@@ -10,9 +10,6 @@ module Mutations
argument :id, ::Types::GlobalIDType[::WorkItem],
required: true, description: 'Global ID of the work item.'
- argument :work_items_ids, [::Types::GlobalIDType[::WorkItem]],
- required: true,
- description: "Global IDs of the items to link. Maximum number of IDs you can provide: #{MAX_WORK_ITEMS}."
field :work_item, Types::WorkItemType,
null: true, description: 'Updated work item.'
@@ -26,7 +23,7 @@ module Mutations
if args[:work_items_ids].size > MAX_WORK_ITEMS
raise Gitlab::Graphql::Errors::ArgumentError,
format(
- _('No more than %{max_work_items} work items can be linked at the same time.'),
+ _('No more than %{max_work_items} work items can be modified at the same time.'),
max_work_items: MAX_WORK_ITEMS
)
end
@@ -50,7 +47,7 @@ module Mutations
private
def update_links(work_item, params)
- raise NotImplementedError
+ raise NotImplementedError, "#{self.class} does not implement #{__method__}"
end
end
end
diff --git a/app/graphql/mutations/work_items/linked_items/remove.rb b/app/graphql/mutations/work_items/linked_items/remove.rb
new file mode 100644
index 00000000000..078f05d2025
--- /dev/null
+++ b/app/graphql/mutations/work_items/linked_items/remove.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Mutations
+ module WorkItems
+ module LinkedItems
+ class Remove < Base
+ graphql_name 'WorkItemRemoveLinkedItems'
+ description 'Remove items linked to the work item.'
+
+ argument :work_items_ids, [::Types::GlobalIDType[::WorkItem]],
+ required: true,
+ description: "Global IDs of the items to unlink. Maximum number of IDs you can provide: #{MAX_WORK_ITEMS}."
+
+ private
+
+ def update_links(work_item, params)
+ gids = params.delete(:work_items_ids)
+ raise Gitlab::Graphql::Errors::ArgumentError, "workItemsIds cannot be empty" if gids.empty?
+
+ work_item_ids = gids.filter_map { |gid| gid.model_id.to_i }
+ ::WorkItems::RelatedWorkItemLinks::DestroyService
+ .new(work_item, current_user, { item_ids: work_item_ids })
+ .execute
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 957fd10690f..b0b29ae8efa 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -181,6 +181,7 @@ module Types
mount_mutation Mutations::WorkItems::Export, alpha: { milestone: '15.10' }
mount_mutation Mutations::WorkItems::Convert, alpha: { milestone: '15.11' }
mount_mutation Mutations::WorkItems::LinkedItems::Add, alpha: { milestone: '16.3' }
+ mount_mutation Mutations::WorkItems::LinkedItems::Remove, alpha: { milestone: '16.3' }
mount_mutation Mutations::SavedReplies::Create
mount_mutation Mutations::SavedReplies::Update
mount_mutation Mutations::Pages::MarkOnboardingComplete
diff --git a/app/helpers/work_items_helper.rb b/app/helpers/work_items_helper.rb
index 9036c7c8347..1969c98de8b 100644
--- a/app/helpers/work_items_helper.rb
+++ b/app/helpers/work_items_helper.rb
@@ -11,4 +11,10 @@ module WorkItemsHelper
report_abuse_path: add_category_abuse_reports_path
}
end
+
+ def work_items_list_data(group)
+ {
+ full_path: group.full_path
+ }
+ end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 7a623b0cefb..720e02de890 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -1039,6 +1039,13 @@ module Ci
end
end
+ def time_in_queue_seconds
+ return if queued_at.nil?
+
+ (::Time.current - queued_at).seconds.to_i
+ end
+ strong_memoize_attr :time_in_queue_seconds
+
protected
def run_status_commit_hooks!
diff --git a/app/models/concerns/linkable_item.rb b/app/models/concerns/linkable_item.rb
index 135252727ab..c91e3615ba7 100644
--- a/app/models/concerns/linkable_item.rb
+++ b/app/models/concerns/linkable_item.rb
@@ -16,6 +16,7 @@ module LinkableItem
scope :for_source, ->(item) { where(source_id: item.id) }
scope :for_target, ->(item) { where(target_id: item.id) }
+ scope :for_source_and_target, ->(source, target) { where(source: source, target: target) }
scope :for_items, ->(source, target) do
where(source: source, target: target).or(where(source: target, target: source))
end
diff --git a/app/models/project.rb b/app/models/project.rb
index ad8757880fd..fdf132fef31 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -81,6 +81,8 @@ class Project < ApplicationRecord
MAX_SUGGESTIONS_TEMPLATE_LENGTH = 255
MAX_COMMIT_TEMPLATE_LENGTH = 500
+ INSTANCE_RUNNER_RUNNING_JOBS_MAX_BUCKET = 5
+
DEFAULT_MERGE_COMMIT_TEMPLATE = <<~MSG.rstrip.freeze
Merge branch '%{source_branch}' into '%{target_branch}'
@@ -3270,6 +3272,13 @@ class Project < ApplicationRecord
errors.add(:path, s_('Project|already in use'))
end
+ def instance_runner_running_jobs_count
+ # excluding currently started job
+ ::Ci::RunningBuild.instance_type.where(project_id: self.id)
+ .limit(INSTANCE_RUNNER_RUNNING_JOBS_MAX_BUCKET + 1).count - 1
+ end
+ strong_memoize_attr :instance_runner_running_jobs_count
+
private
# overridden in EE
diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb
index 79c1946f3d2..838196e96ac 100644
--- a/app/presenters/ci/build_runner_presenter.rb
+++ b/app/presenters/ci/build_runner_presenter.rb
@@ -61,6 +61,16 @@ module Ci
end
# rubocop: enable CodeReuse/ActiveRecord
+ def project_jobs_running_on_instance_runners_count
+ # if not instance runner we don't care about that value and present `+Inf` as a placeholder for Prometheus
+ return '+Inf' unless runner.instance_type?
+
+ return project.instance_runner_running_jobs_count.to_s if
+ project.instance_runner_running_jobs_count < Project::INSTANCE_RUNNER_RUNNING_JOBS_MAX_BUCKET
+
+ "#{Project::INSTANCE_RUNNER_RUNNING_JOBS_MAX_BUCKET}+"
+ end
+
private
def create_archive(artifacts)
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 68ebb376ccd..97e0b2e1a00 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -10,7 +10,7 @@ module Ci
TEMPORARY_LOCK_TIMEOUT = 3.seconds
- Result = Struct.new(:build, :build_json, :valid?)
+ Result = Struct.new(:build, :build_json, :build_presented, :valid?)
##
# The queue depth limit number has been determined by observing 95
@@ -43,7 +43,7 @@ module Ci
if !db_all_caught_up && !result.build
metrics.increment_queue_operation(:queue_replication_lag)
- ::Ci::RegisterJobService::Result.new(nil, nil, false) # rubocop:disable Cop/AvoidReturnFromBlocks
+ ::Ci::RegisterJobService::Result.new(nil, nil, nil, false) # rubocop:disable Cop/AvoidReturnFromBlocks
else
result
end
@@ -86,7 +86,7 @@ module Ci
next unless result
if result.valid?
- @metrics.register_success(result.build)
+ @metrics.register_success(result.build_presented)
@metrics.observe_queue_depth(:found, depth)
return result # rubocop:disable Cop/AvoidReturnFromBlocks
@@ -102,7 +102,7 @@ module Ci
@metrics.observe_queue_depth(:not_found, depth) if valid
@metrics.register_failure
- Result.new(nil, nil, valid)
+ Result.new(nil, nil, nil, valid)
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -159,7 +159,7 @@ module Ci
# this operation.
#
if ::Ci::UpdateBuildQueueService.new.remove!(build)
- return Result.new(nil, nil, false)
+ return Result.new(nil, nil, nil, false)
end
return
@@ -190,11 +190,11 @@ module Ci
# to make sure that this is properly handled by runner.
@metrics.increment_queue_operation(:build_conflict_lock)
- Result.new(nil, nil, false)
+ Result.new(nil, nil, nil, false)
rescue StateMachines::InvalidTransition
@metrics.increment_queue_operation(:build_conflict_transition)
- Result.new(nil, nil, false)
+ Result.new(nil, nil, nil, false)
rescue StandardError => ex
@metrics.increment_queue_operation(:build_conflict_exception)
@@ -221,7 +221,7 @@ module Ci
log_build_dependencies_size(presented_build)
build_json = Gitlab::Json.dump(::API::Entities::Ci::JobRequest::Response.new(presented_build))
- Result.new(build, build_json, true)
+ Result.new(build, build_json, presented_build, true)
end
def log_build_dependencies_size(presented_build)
diff --git a/app/services/work_items/related_work_item_links/destroy_service.rb b/app/services/work_items/related_work_item_links/destroy_service.rb
new file mode 100644
index 00000000000..6d1920d01b2
--- /dev/null
+++ b/app/services/work_items/related_work_item_links/destroy_service.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module WorkItems
+ module RelatedWorkItemLinks
+ class DestroyService < BaseService
+ def initialize(work_item, user, params)
+ @work_item = work_item
+ @current_user = user
+ @params = params.dup
+ @failed_ids = []
+ @removed_ids = []
+ end
+
+ def execute
+ return error(_('No work item found.'), 403) unless can?(current_user, :admin_work_item_link, work_item)
+ return error(_('No work item IDs provided.'), 409) if params[:item_ids].empty?
+
+ destroy_links_for(params[:item_ids])
+
+ if removed_ids.any?
+ success(message: response_message, items_removed: removed_ids, items_with_errors: failed_ids.flatten)
+ else
+ error(error_message)
+ end
+ end
+
+ private
+
+ attr_reader :work_item, :current_user, :failed_ids, :removed_ids
+
+ def destroy_links_for(item_ids)
+ destroy_links(source: work_item, target: item_ids, direction: :target)
+ destroy_links(source: item_ids, target: work_item, direction: :source)
+ end
+
+ def destroy_links(source:, target:, direction:)
+ WorkItems::RelatedWorkItemLink.for_source_and_target(source, target).each do |link|
+ linked_item = link.try(direction)
+
+ if can?(current_user, :admin_work_item_link, linked_item)
+ link.destroy!
+ removed_ids << linked_item.id
+ create_notes(link)
+ else
+ failed_ids << linked_item.id
+ end
+ end
+ end
+
+ def create_notes(link)
+ SystemNoteService.unrelate_issuable(link.source, link.target, current_user)
+ SystemNoteService.unrelate_issuable(link.target, link.source, current_user)
+ end
+
+ def error_message
+ not_linked = params[:item_ids] - (removed_ids + failed_ids)
+ error_messages = []
+
+ if failed_ids.any?
+ error_messages << format(
+ _('%{item_ids} could not be removed due to insufficient permissions'), item_ids: failed_ids.to_sentence
+ )
+ end
+
+ if not_linked.any?
+ error_messages << format(
+ _('%{item_ids} could not be removed due to not being linked'), item_ids: not_linked.to_sentence
+ )
+ end
+
+ return '' unless error_messages.any?
+
+ format(_('IDs with errors: %{error_messages}.'), error_messages: error_messages.join(', '))
+ end
+
+ def response_message
+ success_message = format(_('Successfully unlinked IDs: %{item_ids}.'), item_ids: removed_ids.to_sentence)
+
+ return success_message unless error_message.present?
+
+ "#{success_message} #{error_message}"
+ end
+ end
+ end
+end
diff --git a/app/views/groups/work_items/index.html.haml b/app/views/groups/work_items/index.html.haml
index 2e3d3dda941..299a90b362d 100644
--- a/app/views/groups/work_items/index.html.haml
+++ b/app/views/groups/work_items/index.html.haml
@@ -1,4 +1,4 @@
- page_title s_('WorkItem|Work items')
- add_page_specific_style 'page_bundles/issuable_list'
-.js-work-items-list-root{ data: { full_path: @group.full_path } }
+.js-work-items-list-root{ data: work_items_list_data(@group) }
diff --git a/app/views/projects/merge_requests/_code_dropdown.html.haml b/app/views/projects/merge_requests/_code_dropdown.html.haml
index 4cab6fac388..bfa33f26453 100644
--- a/app/views/projects/merge_requests/_code_dropdown.html.haml
+++ b/app/views/projects/merge_requests/_code_dropdown.html.haml
@@ -1,6 +1,6 @@
.gl-md-ml-3.dropdown.gl-dropdown{ class: "gl-display-none! gl-md-display-flex!" }
#js-check-out-modal{ data: how_merge_modal_data(@merge_request) }
- = button_tag type: 'button', class: "btn dropdown-toggle btn-confirm gl-button gl-dropdown-toggle", data: { toggle: 'dropdown', qa_selector: 'mr_code_dropdown' } do
+ = button_tag type: 'button', class: "btn dropdown-toggle btn-confirm gl-button gl-dropdown-toggle", data: { toggle: 'dropdown', testid: 'mr-code-dropdown' } do
%span.gl-dropdown-button-text= _('Code')
= sprite_icon "chevron-down", size: 16, css_class: "dropdown-icon gl-icon gl-ml-2 gl-mr-0!"
.dropdown-menu.dropdown-menu-right
@@ -16,7 +16,7 @@
= _('Check out branch')
- if current_user
%li.gl-dropdown-item
- = link_to ide_merge_request_path(@merge_request), class: 'dropdown-item', target: '_blank', data: { qa_selector: 'open_in_web_ide_button' } do
+ = link_to ide_merge_request_path(@merge_request), class: 'dropdown-item', target: '_blank', data: { testid: 'open-in-web-ide-button' } do
.gl-dropdown-item-text-wrapper
= _('Open in Web IDE')
- if Gitlab::CurrentSettings.gitpod_enabled && current_user&.gitpod_enabled
@@ -30,10 +30,10 @@
%header.dropdown-header
= _('Download')
%li.gl-dropdown-item
- = link_to merge_request_path(@merge_request, format: :patch), class: 'dropdown-item', download: '', data: { qa_selector: 'download_email_patches_menu_item' } do
+ = link_to merge_request_path(@merge_request, format: :patch), class: 'dropdown-item', download: '', data: { testid: 'download-email-patches-menu-item' } do
.gl-dropdown-item-text-wrapper
= _('Patches')
%li.gl-dropdown-item
- = link_to merge_request_path(@merge_request, format: :diff), class: 'dropdown-item', download: '', data: { qa_selector: 'download_plain_diff_menu_item' } do
+ = link_to merge_request_path(@merge_request, format: :diff), class: 'dropdown-item', download: '', data: { testid: 'download-plain-diff-menu-item' } do
.gl-dropdown-item-text-wrapper
= _('Plain diff')
diff --git a/app/views/projects/merge_requests/_page.html.haml b/app/views/projects/merge_requests/_page.html.haml
index 69e2487152e..dc97aa62c26 100644
--- a/app/views/projects/merge_requests/_page.html.haml
+++ b/app/views/projects/merge_requests/_page.html.haml
@@ -28,12 +28,12 @@
.merge-request-tabs-holder{ class: "#{'js-tabs-affix' unless ENV['RAILS_ENV'] == 'test'} #{'gl-static' if moved_mr_sidebar_enabled?}" }
.merge-request-tabs-container.gl-display-flex.gl-justify-content-space-between{ class: "#{'is-merge-request' if Feature.enabled?(:moved_mr_sidebar, @project) && !fluid_layout}" }
%ul.merge-request-tabs.nav-tabs.nav.nav-links.gl-display-flex.gl-flex-nowrap.gl-m-0.gl-p-0{ class: "#{'gl-w-full gl-lg-w-auto!' if Feature.enabled?(:moved_mr_sidebar, @project)}" }
- = render "projects/merge_requests/tabs/tab", class: "notes-tab", qa_selector: "notes_tab" do
+ = render "projects/merge_requests/tabs/tab", class: "notes-tab", testid: "notes-tab" do
= tab_link_for @merge_request, :show, force_link: @commit.present? do
= _("Overview")
= gl_badge_tag @merge_request.related_notes.user.count, { size: :sm }, { class: 'js-discussions-count' }
- if @merge_request.source_project
- = render "projects/merge_requests/tabs/tab", name: "commits", class: "commits-tab", qa_selector: "commits_tab" do
+ = render "projects/merge_requests/tabs/tab", name: "commits", class: "commits-tab", testid: "commits-tab" do
= tab_link_for @merge_request, :commits do
= _("Commits")
= gl_badge_tag tab_count_display(@merge_request, @commits_count), { size: :sm }, { class: 'js-commits-count' }
@@ -42,7 +42,7 @@
= tab_link_for @merge_request, :pipelines do
= _("Pipelines")
= gl_badge_tag @number_of_pipelines, { size: :sm }, { class: 'js-pipelines-mr-count' }
- = render "projects/merge_requests/tabs/tab", name: "diffs", class: "diffs-tab js-diffs-tab", id: "diffs-tab", qa_selector: "diffs_tab" do
+ = render "projects/merge_requests/tabs/tab", name: "diffs", class: "diffs-tab js-diffs-tab", id: "diffs-tab", testid: "diffs-tab" do
= tab_link_for @merge_request, :diffs do
= _("Changes")
= gl_badge_tag tab_count_display(@merge_request, @diffs_count), { size: :sm }
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index 07bae4d2396..015f6423e7c 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -31,4 +31,4 @@
= form_errors(@merge_request)
.row
.col-12
- = f.submit _('Compare branches and continue'), data: { qa_selector: 'compare_branches_button' }, pajamas_button: true
+ = f.submit _('Compare branches and continue'), data: { testid: 'compare-branches-button' }, pajamas_button: true
diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml
index a7151421acb..996928ba377 100644
--- a/app/views/projects/merge_requests/creations/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml
@@ -50,7 +50,7 @@
= _("Pipelines")
= gl_badge_tag @pipelines.size, { size: :sm }, { class: 'gl-tab-counter-badge' }
%li.diffs-tab
- = link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tabvue', qa_selector: 'diffs_tab'} do
+ = link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tabvue', testid: 'diffs-tab'} do
= _("Changes")
= gl_badge_tag @merge_request.diff_size, { size: :sm }, { class: 'gl-tab-counter-badge' }
diff --git a/app/views/projects/merge_requests/tabs/_tab.html.haml b/app/views/projects/merge_requests/tabs/_tab.html.haml
index 9d942da8098..f6c8f4cd87b 100644
--- a/app/views/projects/merge_requests/tabs/_tab.html.haml
+++ b/app/views/projects/merge_requests/tabs/_tab.html.haml
@@ -1,8 +1,8 @@
- tab_name = local_assigns.fetch(:name, nil)
- tab_class = local_assigns.fetch(:class, nil)
-- qa_selector = local_assigns.fetch(:qa_selector, nil)
+- testid = local_assigns.fetch(:testid, nil)
- id = local_assigns.fetch(:id, nil)
-- attrs = { class: [tab_class, ("active" if params[:tab] == tab_name)], data: { qa_selector: qa_selector } }
+- attrs = { class: [tab_class, ("active" if params[:tab] == tab_name)], data: { testid: testid } }
- attrs[:id] = id if id.present?
%li{ attrs }