diff options
Diffstat (limited to 'app')
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 } |