Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-06-20 18:12:55 +00:00
parent 5c624e3be3
commit 46df8e869e
66 changed files with 526 additions and 624 deletions

View File

@ -137,6 +137,8 @@ export const urlSortParams = {
[RELATIVE_POSITION_ASC]: RELATIVE_POSITION,
[TITLE_ASC]: 'title_asc',
[TITLE_DESC]: 'title_desc',
[START_DATE_ASC]: 'start_date_asc',
[START_DATE_DESC]: 'start_date_desc',
[HEALTH_STATUS_ASC]: 'health_status_asc',
[HEALTH_STATUS_DESC]: 'health_status_desc',
[WEIGHT_ASC]: 'weight',

View File

@ -2,24 +2,13 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import IssuesListApp from 'ee_else_ce/issues/list/components/issues_list_app.vue';
import { resolvers, config } from '~/graphql_shared/issuable_client';
import createDefaultClient, { createApolloClientWithCaching } from '~/lib/graphql';
import { getApolloProvider } from '~/issues/list/issue_client';
import { parseBoolean } from '~/lib/utils/common_utils';
import DesignDetail from '~/work_items/components/design_management/design_preview/design_details.vue';
import { ROUTES } from '~/work_items/constants';
import JiraIssuesImportStatusApp from './components/jira_issues_import_status_app.vue';
import { gqlClient } from './graphql';
let issuesClient;
export async function issuesListClient() {
if (issuesClient) return issuesClient;
issuesClient = gon.features?.frontendCaching
? await createApolloClientWithCaching(resolvers, { localCacheKey: 'issues_list', ...config })
: createDefaultClient(resolvers, config);
return issuesClient;
}
export async function mountJiraIssuesListApp() {
const el = document.querySelector('.js-jira-issues-import-status-root');
@ -124,9 +113,7 @@ export async function mountIssuesListApp() {
return new Vue({
el,
name: 'IssuesListRoot',
apolloProvider: new VueApollo({
defaultClient: await issuesListClient(),
}),
apolloProvider: await getApolloProvider(),
router: new VueRouter({
base: window.location.pathname,
mode: 'history',

View File

@ -0,0 +1,20 @@
import VueApollo from 'vue-apollo';
import { config, defaultClient, resolvers } from '~/graphql_shared/issuable_client';
import { createApolloClientWithCaching } from '~/lib/graphql';
let issuesClientPromise;
async function getIssuesClient() {
if (issuesClientPromise) return issuesClientPromise;
issuesClientPromise = gon.features?.frontendCaching
? createApolloClientWithCaching(resolvers, { localCacheKey: 'issues_list', ...config })
: Promise.resolve(defaultClient);
return issuesClientPromise;
}
export async function getApolloProvider() {
const client = ['projects:issues:index', 'groups:issues'].includes(document.body.dataset.page)
? await getIssuesClient()
: defaultClient;
return new VueApollo({ defaultClient: client });
}

View File

@ -76,6 +76,8 @@ import {
WEIGHT_DESC,
MERGED_AT_ASC,
MERGED_AT_DESC,
START_DATE_ASC,
START_DATE_DESC,
} from './constants';
/**
@ -127,12 +129,17 @@ export const getSortOptions = ({
hasBlockedIssuesFeature,
hasIssuableHealthStatusFeature,
hasIssueWeightsFeature,
hasManualSort = true,
hasMergedDate = false,
hasPriority = true,
hasMilestoneDueDate = true,
hasDueDate = true,
hasLabelPriority = true,
hasManualSort = true,
hasStartDate = false,
hasMergedDate = false,
hasWeight = true,
} = {}) => {
const sortOptions = [
{
hasPriority && {
id: 1,
title: __('Priority'),
sortDirection: {
@ -164,7 +171,7 @@ export const getSortOptions = ({
descending: CLOSED_AT_DESC,
},
},
{
hasMilestoneDueDate && {
id: 5,
title: __('Milestone due date'),
sortDirection: {
@ -188,7 +195,7 @@ export const getSortOptions = ({
descending: POPULARITY_DESC,
},
},
{
hasLabelPriority && {
id: 8,
title: __('Label priority'),
sortDirection: {
@ -212,6 +219,14 @@ export const getSortOptions = ({
descending: TITLE_DESC,
},
},
hasStartDate && {
id: 11,
title: __('Start date'),
sortDirection: {
ascending: START_DATE_ASC,
descending: START_DATE_DESC,
},
},
];
if (hasMergedDate) {
@ -236,7 +251,7 @@ export const getSortOptions = ({
});
}
if (hasIssueWeightsFeature) {
if (hasIssueWeightsFeature && hasWeight) {
sortOptions.push({
id: sortOptions.length + 1,
title: __('Weight'),

View File

@ -51,15 +51,17 @@ export default {
:shape="avatarShape"
aria-hidden="true"
/>
<gl-icon v-if="item.icon" class="gl-mr-3 gl-shrink-0" :name="item.icon" />
<gl-icon v-if="item.icon" class="gl-mr-3 gl-shrink-0" :name="item.icon" data-testid="icon" />
<span class="gl-flex gl-min-w-0 gl-items-center gl-gap-2">
<span v-safe-html="highlightedName" class="gl-truncate gl-text-strong"></span>
<span class="gl-text-subtle" aria-hidden="true">·</span>
<span
v-if="item.namespace"
v-safe-html="item.namespace"
class="gl-truncate gl-text-sm gl-text-subtle"
></span>
<template v-if="item.namespace">
<span class="gl-text-subtle" aria-hidden="true" data-testid="namespace-bullet">·</span>
<span
v-safe-html="item.namespace"
class="gl-truncate gl-text-sm gl-text-subtle"
data-testid="namespace"
></span>
</template>
</span>
</div>
</template>

View File

@ -33,14 +33,12 @@ export default {
<div class="gl-flex gl-flex-row gl-items-center gl-gap-2 gl-truncate">
<span class="gl-truncate"> {{ item.title }} </span>
<span class="gl-text-subtle" aria-hidden="true">·</span>
<div
v-if="item.subtitle"
data-testid="subtitle"
class="gl-truncate gl-text-sm gl-text-subtle"
>
{{ item.subtitle }}
</div>
<template v-if="item.subtitle">
<span class="gl-text-subtle" aria-hidden="true">·</span>
<span data-testid="subtitle" class="gl-truncate gl-text-sm gl-text-subtle">
{{ item.subtitle }}
</span>
</template>
</div>
</search-result-focus-layover>
</template>

View File

@ -1,8 +1,8 @@
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { getApolloProvider } from '~/issues/list/issue_client';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import { JS_TOGGLE_EXPAND_CLASS, CONTEXT_NAMESPACE_GROUPS } from './constants';
import createStore from './components/global_search/store';
import {
@ -94,7 +94,7 @@ export const getSuperSidebarData = () => {
};
};
export const initSuperSidebar = ({
export const initSuperSidebar = async ({
el,
rootPath,
currentPath,
@ -125,7 +125,7 @@ export const initSuperSidebar = ({
return new Vue({
el,
name: 'SuperSidebarRoot',
apolloProvider,
apolloProvider: await getApolloProvider(),
provide: {
rootPath,
currentPath,

View File

@ -452,7 +452,7 @@ export default {
},
titleClassHeader() {
return {
'sm:!gl-hidden gl-mt-3': this.shouldShowAncestors,
'sm:!gl-hidden !gl-mt-3': this.shouldShowAncestors,
'sm:!gl-block': !this.shouldShowAncestors,
'gl-w-full': !this.shouldShowAncestors && !this.editMode,
'editable-wi-title': this.editMode && !this.shouldShowAncestors,
@ -461,7 +461,7 @@ export default {
titleClassComponent() {
return {
'sm:!gl-block': !this.shouldShowAncestors,
'gl-hidden sm:!gl-block gl-mt-3': this.shouldShowAncestors,
'gl-hidden sm:!gl-block !gl-mt-3': this.shouldShowAncestors,
'editable-wi-title': this.workItemsAlphaEnabled,
};
},

View File

@ -1,8 +1,6 @@
import { produce } from 'immer';
import VueApollo from 'vue-apollo';
import { map, isEqual } from 'lodash';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import { issuesListClient } from '~/issues/list';
import { getApolloProvider } from '~/issues/list/issue_client';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { getBaseURL } from '~/lib/utils/url_utility';
@ -854,13 +852,7 @@ export const setNewWorkItemCache = async ({
});
}
const issuesListApolloProvider = new VueApollo({
defaultClient: await issuesListClient(),
});
const cacheProvider = document.querySelector('.js-issues-list-app')
? issuesListApolloProvider
: apolloProvider;
const cacheProvider = await getApolloProvider();
const newWorkItemPath = newWorkItemFullPath(fullPath, workItemType);

View File

@ -37,6 +37,7 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType, withTabs } = {}
labelsManagePath,
registerPath,
signInPath,
hasBlockedIssuesFeature,
hasGroupBulkEditFeature,
hasIterationsFeature,
hasOkrsFeature,
@ -104,6 +105,7 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType, withTabs } = {}
fullPath,
isGroup,
isProject: !isGroup,
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
hasGroupBulkEditFeature: parseBoolean(hasGroupBulkEditFeature),
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasOkrsFeature: parseBoolean(hasOkrsFeature),

View File

@ -1,69 +0,0 @@
import { __ } from '~/locale';
import {
CREATED_ASC,
CREATED_DESC,
DUE_DATE_ASC,
DUE_DATE_DESC,
START_DATE_ASC,
START_DATE_DESC,
TITLE_ASC,
TITLE_DESC,
UPDATED_ASC,
UPDATED_DESC,
} from '~/issues/list/constants';
export const sortOptions = [
{
id: 1,
title: __('Created date'),
sortDirection: {
ascending: CREATED_ASC,
descending: CREATED_DESC,
},
},
{
id: 2,
title: __('Updated date'),
sortDirection: {
ascending: UPDATED_ASC,
descending: UPDATED_DESC,
},
},
{
id: 3,
title: __('Start date'),
sortDirection: {
ascending: START_DATE_ASC,
descending: START_DATE_DESC,
},
},
{
id: 4,
title: __('Due date'),
sortDirection: {
ascending: DUE_DATE_ASC,
descending: DUE_DATE_DESC,
},
},
{
id: 5,
title: __('Title'),
sortDirection: {
ascending: TITLE_ASC,
descending: TITLE_DESC,
},
},
];
export const urlSortParams = {
[CREATED_ASC]: 'created_asc',
[CREATED_DESC]: 'created_date',
[DUE_DATE_ASC]: 'due_date_asc',
[DUE_DATE_DESC]: 'due_date_desc',
[START_DATE_ASC]: 'start_date_asc',
[START_DATE_DESC]: 'start_date_desc',
[TITLE_ASC]: 'title_asc',
[TITLE_DESC]: 'title_desc',
[UPDATED_ASC]: 'updated_asc',
[UPDATED_DESC]: 'updated_desc',
};

View File

@ -12,6 +12,7 @@ import {
getDefaultWorkItemTypes,
getFilterTokens,
getInitialPageParams,
getSortOptions,
getTypeTokenOptions,
} from 'ee_else_ce/issues/list/utils';
import { TYPENAME_NAMESPACE, TYPENAME_USER } from '~/graphql_shared/constants';
@ -33,6 +34,7 @@ import {
PARAM_PAGE_BEFORE,
PARAM_SORT,
PARAM_STATE,
urlSortParams,
} from '~/issues/list/constants';
import searchLabelsQuery from '~/issues/list/queries/search_labels.query.graphql';
import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
@ -100,7 +102,6 @@ import {
WORK_ITEM_TYPE_NAME_KEY_RESULT,
WORK_ITEM_TYPE_NAME_OBJECTIVE,
} from '../constants';
import { sortOptions, urlSortParams } from './list/constants';
const EmojiToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue');
@ -120,7 +121,6 @@ const statusMap = {
export default {
issuableListTabs,
sortOptions,
components: {
GlLoadingIcon,
GlButton,
@ -141,9 +141,12 @@ export default {
'autocompleteAwardEmojisPath',
'canBulkUpdate',
'canBulkEditEpics',
'hasBlockedIssuesFeature',
'hasEpicsFeature',
'hasGroupBulkEditFeature',
'hasIssuableHealthStatusFeature',
'hasIssueDateFilterFeature',
'hasIssueWeightsFeature',
'hasOkrsFeature',
'hasQualityManagementFeature',
'hasCustomFieldsFeature',
@ -581,6 +584,21 @@ export default {
showPageSizeSelector() {
return this.workItems.length > 0;
},
sortOptions() {
return getSortOptions({
hasBlockedIssuesFeature: this.hasBlockedIssuesFeature,
hasIssuableHealthStatusFeature: this.hasIssuableHealthStatusFeature,
hasIssueWeightsFeature: this.hasIssueWeightsFeature,
hasManualSort: false,
hasStartDate: true,
hasPriority: !this.isEpicsList,
hasMilestoneDueDate: Boolean(
!this.isEpicsList || (this.isEpicsList && this.glFeatures.workItemEpicMilestones),
),
hasLabelPriority: !this.isEpicsList,
hasWeight: !this.isEpicsList,
});
},
tabCounts() {
const { all, closed, opened } = this.workItemStateCounts;
return {
@ -880,7 +898,7 @@ export default {
// Trigger pageSize UI component update based on URL changes
this.pageSize = this.pageParams.firstPageSize;
this.sortKey = deriveSortKey({ sort, sortMap: urlSortParams });
this.sortKey = deriveSortKey({ sort });
this.state = state || STATUS_OPEN;
},
checkDrawerParams() {
@ -976,7 +994,7 @@ export default {
:show-page-size-selector="showPageSizeSelector"
:show-pagination-controls="showPaginationControls"
show-work-item-type-icon
:sort-options="$options.sortOptions"
:sort-options="sortOptions"
sync-filter-and-sort
:tab-counts="tabCounts"
:tabs="tabs"

View File

@ -159,7 +159,7 @@ $comparison-empty-state-height: 62px;
@supports (container-type: scroll-state) {
.merge-request-sticky-header {
@include media-breakpoint-up(lg) {
@include media-breakpoint-up(md) {
@apply gl-sticky;
container-type: scroll-state;
container-name: sticky-header;
@ -179,7 +179,7 @@ $comparison-empty-state-height: 62px;
}
@container sticky-header scroll-state(stuck: top) {
&::after {
&::after, .merge-request-sticky-title {
@apply gl-block;
}
@ -199,13 +199,11 @@ $comparison-empty-state-height: 62px;
.merge-request-author-container {
@apply gl-hidden;
}
}
}
@include media-breakpoint-up(lg) {
@container sticky-header scroll-state(stuck: top) {
.merge-request-sticky-title {
@apply gl-block;
@include media-breakpoint-down(md) {
.merge-request-tabs-actions {
@apply gl-hidden;
}
}
}
}

View File

@ -23,7 +23,6 @@ module Glql
# We catch all errors here so they are tracked by SLIs.
# But we only increment the rate limiter failure count for ActiveRecord::QueryAborted.
increment_rate_limit_counter if error.is_a?(ActiveRecord::QueryAborted)
ensure_logs_populated
raise error
ensure
@ -56,19 +55,13 @@ module Glql
def logs
graphql_logs = super.presence || [{}]
graphql_logs.map { |log| log.merge(log_data) }
end
def ensure_logs_populated
RequestStore.store[:graphql_logs] ||= []
RequestStore.store[:graphql_logs] << log_data
end
def log_data
{
glql_referer: request.headers["Referer"],
glql_query_sha: query_sha
}
graphql_logs.map do |log|
log.merge(
glql_referer: request.headers["Referer"],
glql_query_sha: query_sha
)
end
end
def check_rate_limit

View File

@ -47,7 +47,7 @@
= tab_link_for @merge_request, :diffs do
= _("Changes")
= gl_badge_tag tab_count_display(@merge_request, @diffs_count), { class: 'js-changes-tab-count', data: { gid: @merge_request.to_gid.to_s } }
.gl-flex.gl-flex-wrap.gl-items-center.gl-gap-3
.merge-request-tabs-actions.gl-flex.gl-flex-wrap.gl-items-center.gl-gap-3
#js-vue-discussion-counter{ data: { blocks_merge: @project.only_allow_merge_if_all_discussions_are_resolved?.to_s } }
- if !!@issuable_sidebar.dig(:current_user, :id)
.js-sidebar-todo-widget-root{ data: { project_path: @issuable_sidebar[:project_full_path], iid: @issuable_sidebar[:iid], id: @issuable_sidebar[:id] } }

View File

@ -16,26 +16,17 @@ module Projects
tags :import_shared_storage
sidekiq_retries_exhausted do |job, exception|
relation_export = Projects::ImportExport::RelationExport.find(job['args'].first)
project_export_job = relation_export.project_export_job
project = project_export_job.project
relation_export.mark_as_failed(job['error_message'])
log_payload = {
message: 'Project relation export failed',
export_error: job['error_message'],
relation: relation_export.relation,
project_export_job_id: project_export_job.id,
project_name: project.name,
project_id: project.id
}
Gitlab::ExceptionLogFormatter.format!(exception, log_payload)
Gitlab::Export::Logger.error(log_payload)
new.mark_relation_export_failed!(job['args'].first, job['error_message'], exception: exception)
end
def perform(project_relation_export_id, user_id, params = {})
user = User.find(user_id)
if user.banned?
mark_relation_export_failed!(project_relation_export_id, "User #{user_id} is banned")
return
end
params.symbolize_keys!
relation_export = Projects::ImportExport::RelationExport.find(project_relation_export_id)
@ -47,6 +38,26 @@ module Projects
Projects::ImportExport::RelationExportService.new(relation_export, user, jid, params).execute
end
end
def mark_relation_export_failed!(project_relation_export_id, message, exception: nil)
relation_export = Projects::ImportExport::RelationExport.find(project_relation_export_id)
project_export_job = relation_export.project_export_job
project = project_export_job.project
relation_export.mark_as_failed(message)
log_payload = {
message: 'Project relation export failed',
export_error: message,
relation: relation_export.relation,
project_export_job_id: project_export_job.id,
project_name: project.name,
project_id: project.id
}
Gitlab::ExceptionLogFormatter.format!(exception, log_payload) if exception.present?
Gitlab::Export::Logger.error(log_payload)
end
end
end
end

View File

@ -5,4 +5,4 @@ feature_category: groups_and_projects
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/183967
milestone: '17.11'
queued_migration_version: 20250403051401
finalized_by: # version of the migration that finalized this BBM
finalized_by: '20250618220559'

View File

@ -5,4 +5,4 @@ feature_category: feature_flags
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/180223
milestone: '17.9'
queued_migration_version: 20250204151548
finalized_by: # version of the migration that finalized this BBM
finalized_by: '20250619145012'

View File

@ -5,4 +5,4 @@ feature_category: package_registry
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/185222
milestone: '17.11'
queued_migration_version: 20250320085452
finalized_by: # version of the migration that finalized this BBM
finalized_by: '20250616173500'

View File

@ -6,14 +6,20 @@ Gitlab::Seeder.quiet do
return
end
password_via_env = ENV['GITLAB_ROOT_PASSWORD'].presence
password = password_via_env || '5iveL!fe'
# When password is set via environment variable, don't force reset on first login.
# Otherwise, expire password immediately to require reset.
password_expires_at = DateTime.now unless password_via_env
admin = User.create!(
name: 'Administrator',
email: "gitlab_admin_#{SecureRandom.hex(3)}@example.com",
username: 'root',
password: '5iveL!fe',
password: password,
admin: true,
confirmed_at: DateTime.now,
password_expires_at: DateTime.now
password_expires_at: password_expires_at
) do |user|
user.assign_personal_namespace(Organizations::Organization.default_organization)
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class FinalizeBackfillPackagesConanFileMetadataProjectId < Gitlab::Database::Migration[2.3]
milestone '18.2'
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
def up
ensure_batched_background_migration_is_finished(
job_class_name: 'BackfillPackagesConanFileMetadataProjectId',
table_name: :packages_conan_file_metadata,
column_name: :id,
job_arguments: [:project_id, :packages_package_files, :project_id, :package_file_id],
finalize: true
)
end
def down; end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class FinalizeBackfillNamespacesRedirectRoutesNamespaceId < Gitlab::Database::Migration[2.3]
milestone '18.2'
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
def up
ensure_batched_background_migration_is_finished(
job_class_name: 'BackfillNamespacesRedirectRoutesNamespaceId',
table_name: :redirect_routes,
column_name: :id,
job_arguments: [],
finalize: true
)
end
def down; end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class FinalizeBackfillOperationsScopesProjectId < Gitlab::Database::Migration[2.3]
milestone '18.2'
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
def up
ensure_batched_background_migration_is_finished(
job_class_name: 'BackfillOperationsScopesProjectId',
table_name: :operations_scopes,
column_name: :id,
job_arguments: [:project_id, :operations_strategies, :project_id, :strategy_id],
finalize: true
)
end
def down; end
end

View File

@ -0,0 +1 @@
0233515b55c32b5ebcb73dfec6aa362f107a10538484cac3ee0e36833175c2ef

View File

@ -0,0 +1 @@
a5521a381e16c988f130e632991eb22eadb7a5f500a4cc429fedf72ddfce9f53

View File

@ -0,0 +1 @@
81c5ba0591c9e439013df34658b86dfbbc635c667b1928652bca0cbaffb82ba4

View File

@ -1,6 +1,6 @@
---
stage: Software Supply Chain Security
group: Authentication
stage: Fulfillment
group: Provision
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
gitlab_dedicated: no
title: LDAP synchronization

View File

@ -1,6 +1,6 @@
---
stage: Software Supply Chain Security
group: Authentication
group: Authorization
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
title: Guest users
---

View File

@ -3,6 +3,7 @@ stage: Foundations
group: Import and Integrate
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
title: Extend with GitLab
description: Connect GitLab to your tools and workflows to build a customized development environment.
---
Connect GitLab to your tools and workflows to build a customized development environment.

View File

@ -1,6 +1,6 @@
---
stage: Software Supply Chain Security
group: Authentication
group: Authorization
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
title: Member roles API
---

View File

@ -1,6 +1,6 @@
---
stage: Software Supply Chain Security
group: Authentication
stage: Tenant Scale
group: Organizations
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
title: Group and project members API
---

View File

@ -2,8 +2,8 @@
stage: none
group: unassigned
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review.
description: 'Development Guidelines: learn how to contribute to GitLab.'
title: Contribute to development
description: Learn how to contribute to the development of the GitLab product.
---
Learn how to contribute to the development of the GitLab product.

View File

@ -1,6 +1,6 @@
---
stage: Software Supply Chain Security
group: Authentication
group: Authorization
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review.
title: The `DeclarativePolicy` framework
---

View File

@ -1,6 +1,6 @@
---
stage: Software Supply Chain Security
group: Authentication
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
title: Rate limits
---

View File

@ -1,6 +1,6 @@
---
stage: Software Supply Chain Security
group: Authentication
stage: Plan
group: Project Management
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
title: User file uploads
---

View File

@ -3,6 +3,7 @@ stage: Solutions Architecture
group: Solutions Architecture
info: This page is owned by the Solutions Architecture team.
title: Solutions architecture
description: Use these reference solutions to integrate GitLab with your people, process, and technology.
---
As with all extensible platforms, GitLab has many features that can be creatively combined together with third party functionality to create solutions that address the specific people, process, and technology challenges of the organizations that use it. Reference solutions and implementations can also be crafted at a more general level so that they can be adopted and customized by customers with similar needs to the reference solution.

View File

@ -3,6 +3,7 @@ stage: Fulfillment
group: Subscription Management
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
title: Subscribe to GitLab
description: Choose and manage the subscription that's right for you and your organization.
---
Choose and manage the subscription that's right for you and your organization.

View File

@ -3,9 +3,10 @@ stage: none
group: Tutorials
info: For assistance with this tutorials page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-other-projects-and-subjects.
title: Learn GitLab with tutorials
description: Help learn key GitLab workflows by following guided instructions.
---
These tutorials can help you learn how to use GitLab.
Learn about GitLab fundamentals by following guided instructions.
{{< cards >}}

View File

@ -3,6 +3,7 @@ stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
title: Use GitLab
description: Get to know the GitLab end-to-end workflow.
---
Get to know the GitLab end-to-end workflow. Configure permissions,

View File

@ -92,6 +92,7 @@ Details of each dependency are listed, sorted by decreasing severity of vulnerab
- [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/513321) in GitLab 17.10. Feature flag `project_component_filter` removed.
- Dependency version filtering introduced for [projects](https://gitlab.com/gitlab-org/gitlab/-/issues/520771) and [groups](https://gitlab.com/gitlab-org/gitlab/-/issues/523061) in GitLab 18.0 with [flags](../../../administration/feature_flags/_index.md) named `version_filtering_on_project_level_dependency_list` and `version_filtering_on_group_level_dependency_list`. Disabled by default.
- Dependency version filtering [enabled](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/192291) on GitLab.com, GitLab Self-Managed, and GitLab Dedicated in GitLab 18.1.
- Feature flags `version_filtering_on_project_level_dependency_list` and `version_filtering_on_group_level_dependency_list` removed.
{{< /history >}}

View File

@ -2,7 +2,7 @@
stage: AI-powered
group: AI Framework
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
description: AI-native features and functionality.
description: Get help from a suite of AI-native features while you work in GitLab.
title: GitLab Duo
---

View File

@ -1,6 +1,6 @@
---
stage: Tenant Scale
group: Organizations
group: Authentication
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
title: Achievements
---

View File

@ -1,6 +1,6 @@
---
stage: Tenant Scale
group: Organizations
group: Authentication
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
title: Contributions calendar
---

View File

@ -6,7 +6,6 @@ module Gitlab
module Commands
autoload :BackupSubcommand, 'gitlab/backup/cli/commands/backup_subcommand'
autoload :Command, 'gitlab/backup/cli/commands/command'
autoload :ObjectStorageCommand, 'gitlab/backup/cli/commands/object_storage_command'
autoload :RestoreSubcommand, 'gitlab/backup/cli/commands/restore_subcommand'
end
end

View File

@ -4,7 +4,7 @@ module Gitlab
module Backup
module Cli
module Commands
class BackupSubcommand < ObjectStorageCommand
class BackupSubcommand < Command
package_name 'Backup'
desc 'all', 'Creates a backup including repositories, database and local files'

View File

@ -1,31 +0,0 @@
# frozen_string_literal: true
module Gitlab
module Backup
module Cli
module Commands
class ObjectStorageCommand < Command
class_option :backup_bucket,
desc: "When backing up object storage, this is the bucket to backup to",
required: false,
type: :string
class_option :wait_for_completion,
desc: "Wait for object storage backups to complete",
type: :boolean,
default: true
class_option :registry_bucket,
desc: "When backing up registry from object storage, this is the source bucket",
required: false,
type: :string
class_option :service_account_file,
desc: "JSON file containing the Google service account credentials",
default: "/etc/gitlab/backup-account-credentials.json",
type: :string
end
end
end
end
end

View File

@ -4,7 +4,7 @@ module Gitlab
module Backup
module Cli
module Commands
class RestoreSubcommand < ObjectStorageCommand
class RestoreSubcommand < Command
package_name 'Restore'
desc 'all BACKUP_ID', 'Restores a backup including repositories, database and local files'

View File

@ -136,6 +136,14 @@ module Gitlab
raise ::Gitlab::Backup::Cli::Error, 'GITLAB_PATH is missing'
end
def registry_enabled?
gitlab_config.dig(env, 'registry', 'enabled')
end
def object_storage_connection
gitlab_config.dig(env, 'object_storage', 'connection')
end
private
# Return the shared path used as a fallback base location to each blob type

View File

@ -7,7 +7,6 @@ module Gitlab
autoload :Target, 'gitlab/backup/cli/targets/target'
autoload :Database, 'gitlab/backup/cli/targets/database'
autoload :Files, 'gitlab/backup/cli/targets/files'
autoload :ObjectStorage, 'gitlab/backup/cli/targets/object_storage'
autoload :Repositories, 'gitlab/backup/cli/targets/repositories'
end
end

View File

@ -1,26 +0,0 @@
# frozen_string_literal: true
require "google/cloud/storage_transfer"
require_relative "object_storage/google"
module Gitlab
module Backup
module Cli
module Targets
class ObjectStorage
SUPPORTED_PROVIDERS = [
"Google"
].freeze
def self.find_task(object_type, options, config)
# For objects that don't use the consolidated config (like the registry), try the global
# object_store for connection information. This will go away with a config file
# https://gitlab.com/gitlab-org/gitlab/-/issues/475114
const_get(config.object_store.connection.provider, false).new(object_type, options, config)
end
end
end
end
end
end

View File

@ -1,138 +0,0 @@
# frozen_string_literal: true
require "google/cloud/storage_transfer"
module Gitlab
module Backup
module Cli
module Targets
class ObjectStorage
class Google < Target
OperationNotFoundError = Class.new(StandardError)
attr_accessor :object_type, :backup_bucket, :client, :config, :results
def initialize(object_type, remote_directory, config)
@object_type = object_type
@backup_bucket = remote_directory
@config = config
@client = ::Google::Cloud::StorageTransfer.storage_transfer_service
end
# @param [String] backup_id unique identifier for the backup
def dump(backup_id)
response = find_or_create_job(backup_id, "backup")
run_request = {
project_id: backup_job_spec(backup_id)[:project_id],
job_name: response.name
}
@results = client.run_transfer_job run_request
end
# @param [String] backup_id unique identifier for the backup
def restore(backup_id)
response = find_or_create_job(backup_id, "restore")
run_request = {
project_id: restore_job_spec(backup_id)[:project_id],
job_name: response.name
}
@results = client.run_transfer_job run_request
end
def job_name(operation)
"transferJobs/#{object_type}-#{operation}"
end
def backup_job_spec(backup_id)
job_spec(
config.object_store.remote_directory,
backup_bucket,
operation: "backup",
destination_path: backup_path(backup_id)
)
end
def restore_job_spec(backup_id)
job_spec(
backup_bucket,
config.object_store.remote_directory,
operation: "restore",
source_path: backup_path(backup_id)
)
end
def backup_path(backup_id)
"backups/#{backup_id}/#{object_type}/"
end
def find_job_spec(backup_id, operation)
case operation
when "backup"
backup_job_spec(backup_id)
when "restore"
restore_job_spec(backup_id)
else
raise StandardError "Operation #{operation} not found"
end
end
def job_spec(source, destination, operation:, source_path: nil, destination_path: nil)
{
project_id: config.object_store.connection.google_project,
name: job_name(operation),
transfer_spec: {
gcs_data_source: {
bucket_name: source,
path: source_path
},
gcs_data_sink: {
bucket_name: destination,
# NOTE: The trailing '/' is required
path: destination_path
}
},
status: :ENABLED
}
end
def asynchronous?
true
end
def wait_until_done!
@results.wait_until_done!
end
private
def find_or_create_job(backup_id, operation)
begin
name = job_name(operation)
response = client.get_transfer_job(
job_name: name, project_id: config.object_store.connection.google_project
)
log.info("Existing job for #{object_type} found, using")
job_update = find_job_spec(backup_id, operation)
job_update.delete(:project_id)
client.update_transfer_job(
job_name: name,
project_id: config.object_store.connection.google_project,
transfer_job: job_update
)
rescue ::Google::Cloud::NotFoundError
log.info("Existing job for #{object_type} not found, creating one")
response = client.create_transfer_job transfer_job: find_job_spec(backup_id, operation)
end
response
end
def log
Gitlab::Backup::Cli::Output
end
end
end
end
end
end
end

View File

@ -7,7 +7,7 @@ module Gitlab
class Registry < Task
def self.id = 'registry'
def enabled = Gitlab.config.registry.enabled
def enabled = context.registry_enabled?
def human_name = 'Container Registry Images'
@ -25,13 +25,17 @@ module Gitlab
# Registry does not use consolidated object storage config.
def config
settings = {
object_store: {
connection: context.gitlab_config('object_store').connection.to_hash,
unless context
Output.warning("No context passed to derive configuration from.")
return nil
end
{
object_storage: {
connection: context.object_storage_connection,
remote_directory: registry_bucket
}
}
GitlabSettings::Options.build(settings)
end
private

View File

@ -74,17 +74,6 @@ module Gitlab
nil
end
def object_storage?
return false unless config
return false unless config.respond_to?(:object_store) && config.object_store.enabled
return false unless Gitlab::Backup::Cli::Targets::ObjectStorage::SUPPORTED_PROVIDERS.include?(
config.object_store.connection.provider
)
true
end
def asynchronous?
target.asynchronous? || false
end
@ -96,13 +85,7 @@ module Gitlab
def target
return @target unless @target.nil?
@target = if object_storage?
::Gitlab::Backup::Cli::Targets::ObjectStorage.find_task(id, options, config)
else
local
end
@target
@target ||= local
end
private

View File

@ -1,133 +0,0 @@
# frozen_string_literal: true
RSpec.describe Gitlab::Backup::Cli::Targets::ObjectStorage::Google do
let(:gitlab_config) { class_double("GitlabSettings::Settings") }
let(:supported_config) { instance_double("GitlabSettings::Options", object_store: supported_object_store) }
let(:supported_provider) do
instance_double(
"GitlabSettings::Options", provider: "Google", google_application_default: true, google_project: "fake_project"
)
end
let(:supported_object_store) do
instance_double(
"GitlabSettings::Options", enabled: true, connection: supported_provider, remote_directory: "fake_source_bucket"
)
end
let(:client) { instance_double("::Google::Cloud::StorageTransfer::V1::StorageTransferService::Client") }
let(:backup_transfer_job) { build(:google_cloud_storage_transfer_job) }
let(:restore_transfer_job) { build(:google_cloud_storage_transfer_job) }
let(:new_backup_transfer_job_spec) do
{
name: "transferJobs/fake_object-backup",
project_id: "fake_project",
transfer_spec: {
gcs_data_source: {
bucket_name: "fake_source_bucket",
path: nil
},
gcs_data_sink: {
bucket_name: "fake_backup_bucket",
path: "backups/12345/fake_object/"
}
},
status: :ENABLED
}
end
let(:new_restore_transfer_job_spec) do
{
name: "transferJobs/fake_object-restore",
project_id: "fake_project",
transfer_spec: {
gcs_data_source: {
bucket_name: "fake_backup_bucket",
path: "backups/12345/fake_object/"
},
gcs_data_sink: {
bucket_name: "fake_source_bucket",
path: nil
}
},
status: :ENABLED
}
end
before do
allow(Gitlab).to receive(:config).and_return(gitlab_config)
allow(::Google::Cloud::StorageTransfer).to receive(:storage_transfer_service).and_return(client)
allow(gitlab_config).to receive(:[]).with('fake_object').and_return(supported_config)
end
subject(:object_storage) { described_class.new("fake_object", 'fake_backup_bucket', supported_config) }
describe "#dump", :silence_output do
context "when job exists" do
before do
allow(client).to receive(:get_transfer_job).and_return(backup_transfer_job)
end
it "reuses existing job" do
updated_spec = new_backup_transfer_job_spec
expect(client).to receive(:update_transfer_job).with(
job_name: updated_spec[:name],
project_id: updated_spec.delete(:project_id),
transfer_job: updated_spec
)
expect(client).to receive(:run_transfer_job).with({ job_name: "fake_transfer_job", project_id: "fake_project" })
object_storage.dump(12345)
end
end
context "when job does not exist" do
before do
allow(client).to receive(:get_transfer_job).with(
job_name: "transferJobs/fake_object-backup", project_id: "fake_project"
).and_raise(::Google::Cloud::NotFoundError)
allow(client).to receive(:run_transfer_job)
end
it "creates a new job" do
expect(client).to receive(:create_transfer_job)
.with(transfer_job: new_backup_transfer_job_spec).and_return(backup_transfer_job)
object_storage.dump(12345)
end
end
end
describe "#restore", :silence_output do
context "when job exists" do
before do
allow(client).to receive(:get_transfer_job).and_return(restore_transfer_job)
end
it "reuses existing job" do
updated_spec = new_restore_transfer_job_spec
expect(client).to receive(:update_transfer_job).with(
job_name: updated_spec[:name],
project_id: updated_spec.delete(:project_id),
transfer_job: updated_spec
)
expect(client).to receive(:run_transfer_job).with({ job_name: "fake_transfer_job", project_id: "fake_project" })
object_storage.restore(12345)
end
end
context "when job does not exist" do
before do
allow(client).to receive(:get_transfer_job).with(
job_name: "transferJobs/fake_object-restore", project_id: "fake_project"
).and_raise(::Google::Cloud::NotFoundError)
allow(client).to receive(:run_transfer_job)
end
it "creates a new job" do
expect(client).to receive(:create_transfer_job)
.with(transfer_job: new_restore_transfer_job_spec).and_return(restore_transfer_job)
object_storage.restore(12345)
end
end
end
end

View File

@ -13,14 +13,6 @@ RSpec.describe 'gitlab-backup-cli backup subcommand', type: :thor do
gitlab-backup-cli backup all # Creates a backup including repositories, database and local files
gitlab-backup-cli backup help [COMMAND] # Describe subcommands or one specific subcommand
Options:
[--backup-bucket=BACKUP_BUCKET] # When backing up object storage, this is the bucket to backup to
[--wait-for-completion], [--no-wait-for-completion], [--skip-wait-for-completion] # Wait for object storage backups to complete
# Default: true
[--registry-bucket=REGISTRY_BUCKET] # When backing up registry from object storage, this is the source bucket
[--service-account-file=SERVICE_ACCOUNT_FILE] # JSON file containing the Google service account credentials
# Default: /etc/gitlab/backup-account-credentials.json
COMMAND
end

View File

@ -13,14 +13,6 @@ RSpec.describe 'gitlab-backup-cli restore subcommand', type: :thor do
gitlab-backup-cli restore all BACKUP_ID # Restores a backup including repositories, database and local files
gitlab-backup-cli restore help [COMMAND] # Describe subcommands or one specific subcommand
Options:
[--backup-bucket=BACKUP_BUCKET] # When backing up object storage, this is the bucket to backup to
[--wait-for-completion], [--no-wait-for-completion], [--skip-wait-for-completion] # Wait for object storage backups to complete
# Default: true
[--registry-bucket=REGISTRY_BUCKET] # When backing up registry from object storage, this is the source bucket
[--service-account-file=SERVICE_ACCOUNT_FILE] # JSON file containing the Google service account credentials
# Default: /etc/gitlab/backup-account-credentials.json
COMMAND
end

View File

@ -60,7 +60,7 @@
"@gitlab/application-sdk-browser": "^0.3.4",
"@gitlab/at.js": "1.5.7",
"@gitlab/cluster-client": "^3.0.0",
"@gitlab/duo-ui": "^8.18.0",
"@gitlab/duo-ui": "^8.22.0",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0",
"@gitlab/query-language-rust": "0.9.0",

View File

@ -96,10 +96,21 @@ function parseArgumentsAndEnvironment() {
}
}
const coverageBranchExclusions = [
/^as-if-foss\//,
/^\d+-\d+-stable(-ee)?$/, // exclude stable branches like 17-10-stable-ee, 18-0-stable-ee, etc.
];
// Enable coverage if:
// - explicitly requested via --coverage
// - not running in a merge request (CI_MERGE_REQUEST_IID is unset)
// - the current branch is not excluded by coverageBranchExclusions
// - not running under Vue 3 (Vue 3 tests are not currently covered)
// - not running in predictive mode (predictive mode does not support coverage)
const coverage =
options.coverage ||
(!process.env.CI_MERGE_REQUEST_IID &&
!/^as-if-foss\//.test(process.env.CI_COMMIT_BRANCH) &&
!coverageBranchExclusions.some((rule) => rule.test(process.env.CI_COMMIT_BRANCH)) &&
!options.vue3 &&
!options.predictive);

View File

@ -37,7 +37,6 @@ ee/spec/frontend/metrics/details/metrics_details_spec.js
ee/spec/frontend/metrics/details/related_traces_spec.js
ee/spec/frontend/ml/ai_agents/views/edit_agent_spec.js
ee/spec/frontend/oncall_schedule/schedule/components/preset_days/days_header_sub_item_spec.js
ee/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
ee/spec/frontend/related_items_tree/components/related_items_tree_body_spec.js
ee/spec/frontend/related_items_tree/components/tree_root_spec.js
ee/spec/frontend/roadmap/components/roadmap_shell_spec.js

View File

@ -90,19 +90,6 @@ RSpec.describe Glql::BaseController, feature_category: :integrations do
execute_request
end
it 'tracks glql related logs' do
RequestStore.clear!
execute_request
expect(RequestStore.store[:graphql_logs]).to match([
hash_including(
glql_referer: anything,
glql_query_sha: anything
)
])
end
end
context 'when 2 consecutive ActiveRecord::QueryAborted errors occur' do

View File

@ -22,12 +22,6 @@ exports[`SearchItem should render the item 1`] = `
>
Cole Dickinson
</span>
<span
aria-hidden="true"
class="gl-text-subtle"
>
·
</span>
</span>
</div>
`;
@ -38,6 +32,7 @@ exports[`SearchItem should render the item 2`] = `
>
<gl-icon-stub
class="gl-mr-3 gl-shrink-0"
data-testid="icon"
name="users"
size="16"
variant="current"
@ -50,12 +45,6 @@ exports[`SearchItem should render the item 2`] = `
>
Manage &gt; Activity
</span>
<span
aria-hidden="true"
class="gl-text-subtle"
>
·
</span>
</span>
</div>
`;
@ -85,11 +74,13 @@ exports[`SearchItem should render the item 3`] = `
<span
aria-hidden="true"
class="gl-text-subtle"
data-testid="namespace-bullet"
>
·
</span>
<span
class="gl-text-sm gl-text-subtle gl-truncate"
data-testid="namespace"
>
Gitlab Org / MockProject1
</span>
@ -119,12 +110,6 @@ exports[`SearchItem should render the item 4`] = `
>
Dismiss Cipher with no integrity
</span>
<span
aria-hidden="true"
class="gl-text-subtle"
>
·
</span>
</span>
</div>
`;

View File

@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import { GlAvatar } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SearchItem from '~/super_sidebar/components/global_search/command_palette/search_item.vue';
import { getFormattedItem } from '~/super_sidebar/components/global_search/utils';
import { linksReducer } from '~/super_sidebar/components/global_search/command_palette/utils';
@ -17,7 +18,7 @@ describe('SearchItem', () => {
let wrapper;
const createComponent = (item) => {
wrapper = shallowMount(SearchItem, {
wrapper = shallowMountExtended(SearchItem, {
propsData: {
item,
searchQuery: 'root',
@ -25,9 +26,87 @@ describe('SearchItem', () => {
});
};
const findAvatar = () => wrapper.findComponent(GlAvatar);
const findIcon = () => wrapper.findByTestId('icon');
const findNamespace = () => wrapper.findByTestId('namespace');
const findNamespaceBullet = () => wrapper.findByTestId('namespace-bullet');
it.each([mockUser, mockCommand, mockProject, mockIssue])('should render the item', (item) => {
createComponent(item);
expect(wrapper.element).toMatchSnapshot();
});
describe('item rendering', () => {
it('should render avatar when avatar_url is provided', () => {
const item = {
text: 'Test Item',
avatar_url: 'https://example.com/avatar.png',
entity_id: '123',
entity_name: 'Test Entity',
};
createComponent(item);
expect(findAvatar().props('src')).toBe(item.avatar_url);
expect(findAvatar().props('entityId')).toBe(item.entity_id);
expect(findAvatar().props('entityName')).toBe(item.entity_name);
expect(findAvatar().props('size')).toBe(16);
expect(findAvatar().attributes('aria-hidden')).toBe('true');
});
it('should not render avatar when avatar_url is undefined', () => {
const item = {
text: 'Test Item',
};
createComponent(item);
expect(findAvatar().exists()).toBe(false);
});
it('should render icon when present', () => {
const item = {
icon: 'search-results',
text: 'Test Item',
};
createComponent(item);
expect(findIcon().props('name')).toBe(item.icon);
});
it('should not render icon when not present', () => {
const item = {
text: 'Test Item',
};
createComponent(item);
expect(findIcon().exists()).toBe(false);
});
it('should render namespace when present', () => {
const item = {
text: 'Test Item',
namespace: 'test-namespace',
};
createComponent(item);
expect(findNamespaceBullet().exists()).toBe(true);
expect(findNamespace().text()).toBe('test-namespace');
});
it('should not render namespace when not present', () => {
const item = {
text: 'Test Item',
};
createComponent(item);
expect(findNamespaceBullet().exists()).toBe(false);
expect(findNamespace().exists()).toBe(false);
});
});
});

View File

@ -501,7 +501,7 @@ describe('WorkItemDetail component', () => {
});
it('does not show title in the header when parent exists', () => {
expect(findWorkItemType().classes()).toEqual(['sm:!gl-hidden', 'gl-mt-3']);
expect(findWorkItemType().classes()).toEqual(['sm:!gl-hidden', '!gl-mt-3']);
});
});
@ -518,7 +518,7 @@ describe('WorkItemDetail component', () => {
});
it('does not show title in the header when parent exists', () => {
expect(findWorkItemType().classes()).toEqual(['sm:!gl-hidden', 'gl-mt-3']);
expect(findWorkItemType().classes()).toEqual(['sm:!gl-hidden', '!gl-mt-3']);
});
});
});

View File

@ -21,7 +21,7 @@ import {
import setWindowLocation from 'helpers/set_window_location_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
import { CREATED_DESC, UPDATED_DESC } from '~/issues/list/constants';
import { CREATED_DESC, UPDATED_DESC, urlSortParams } from '~/issues/list/constants';
import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { getParameterByName, removeParams, updateHistory } from '~/lib/utils/url_utility';
@ -47,7 +47,6 @@ import {
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import CreateWorkItemModal from '~/work_items/components/create_work_item_modal.vue';
import WorkItemsListApp from '~/work_items/pages/work_items_list_app.vue';
import { sortOptions, urlSortParams } from '~/work_items/pages/list/constants';
import getWorkItemStateCountsQuery from 'ee_else_ce/work_items/graphql/list/get_work_item_state_counts.query.graphql';
import getWorkItemsFullQuery from 'ee_else_ce/work_items/graphql/list/get_work_items_full.query.graphql';
import getWorkItemsSlimQuery from 'ee_else_ce/work_items/graphql/list/get_work_items_slim.query.graphql';
@ -142,8 +141,12 @@ describeSkipVue3(skipReason, () => {
autocompleteAwardEmojisPath: 'autocomplete/award/emojis/path',
canBulkUpdate: true,
canBulkEditEpics: true,
hasBlockedIssuesFeature: false,
hasEpicsFeature: false,
hasGroupBulkEditFeature: true,
hasIssuableHealthStatusFeature: false,
hasIssueDateFilterFeature: false,
hasIssueWeightsFeature: false,
hasOkrsFeature: false,
hasQualityManagementFeature: false,
hasCustomFieldsFeature: false,
@ -152,8 +155,6 @@ describeSkipVue3(skipReason, () => {
isSignedIn: true,
showNewWorkItem: true,
workItemType: null,
hasIssueDateFilterFeature: false,
timeTrackingLimitToHours: false,
...provide,
},
propsData: {
@ -205,7 +206,6 @@ describeSkipVue3(skipReason, () => {
namespace: 'work-items',
recentSearchesStorageKey: 'issues',
showWorkItemTypeIcon: true,
sortOptions,
tabs: WorkItemsListApp.issuableListTabs,
});
});
@ -260,6 +260,124 @@ describeSkipVue3(skipReason, () => {
});
});
describe('sort options', () => {
describe('when all features are enabled', () => {
it('renders all sort options', async () => {
mountComponent({
provide: {
hasBlockedIssuesFeature: true,
hasIssuableHealthStatusFeature: true,
hasIssueWeightsFeature: true,
},
});
await waitForPromises();
expect(findIssuableList().props('sortOptions')).toEqual([
expect.objectContaining({ title: 'Priority' }),
expect.objectContaining({ title: 'Created date' }),
expect.objectContaining({ title: 'Updated date' }),
expect.objectContaining({ title: 'Closed date' }),
expect.objectContaining({ title: 'Milestone due date' }),
expect.objectContaining({ title: 'Due date' }),
expect.objectContaining({ title: 'Popularity' }),
expect.objectContaining({ title: 'Label priority' }),
expect.objectContaining({ title: 'Title' }),
expect.objectContaining({ title: 'Start date' }),
expect.objectContaining({ title: 'Health' }),
expect.objectContaining({ title: 'Weight' }),
expect.objectContaining({ title: 'Blocking' }),
]);
});
});
describe('when all features are not enabled', () => {
it('renders base sort options', async () => {
mountComponent({
provide: {
hasBlockedIssuesFeature: false,
hasIssuableHealthStatusFeature: false,
hasIssueWeightsFeature: false,
},
});
await waitForPromises();
expect(findIssuableList().props('sortOptions')).toEqual([
expect.objectContaining({ title: 'Priority' }),
expect.objectContaining({ title: 'Created date' }),
expect.objectContaining({ title: 'Updated date' }),
expect.objectContaining({ title: 'Closed date' }),
expect.objectContaining({ title: 'Milestone due date' }),
expect.objectContaining({ title: 'Due date' }),
expect.objectContaining({ title: 'Popularity' }),
expect.objectContaining({ title: 'Label priority' }),
expect.objectContaining({ title: 'Title' }),
expect.objectContaining({ title: 'Start date' }),
]);
});
});
describe('when epics list', () => {
describe('when workItemEpicMilestones is disabled', () => {
it('does not render "Priority", "Milestone due date", "Label priority", and "Weight" sort options', async () => {
mountComponent({
provide: {
glFeatures: {
workItemEpicMilestones: false,
},
hasBlockedIssuesFeature: true,
hasIssuableHealthStatusFeature: true,
hasIssueWeightsFeature: true,
workItemType: WORK_ITEM_TYPE_NAME_EPIC,
},
});
await waitForPromises();
expect(findIssuableList().props('sortOptions')).toEqual([
expect.objectContaining({ title: 'Created date' }),
expect.objectContaining({ title: 'Updated date' }),
expect.objectContaining({ title: 'Closed date' }),
expect.objectContaining({ title: 'Due date' }),
expect.objectContaining({ title: 'Popularity' }),
expect.objectContaining({ title: 'Title' }),
expect.objectContaining({ title: 'Start date' }),
expect.objectContaining({ title: 'Health' }),
expect.objectContaining({ title: 'Blocking' }),
]);
});
});
describe('when workItemEpicMilestones is enabled', () => {
it('does not render "Priority", "Label priority", and "Weight" sort options', async () => {
mountComponent({
provide: {
glFeatures: {
workItemEpicMilestones: true,
},
hasBlockedIssuesFeature: true,
hasIssuableHealthStatusFeature: true,
hasIssueWeightsFeature: true,
workItemType: WORK_ITEM_TYPE_NAME_EPIC,
},
});
await waitForPromises();
expect(findIssuableList().props('sortOptions')).toEqual([
expect.objectContaining({ title: 'Created date' }),
expect.objectContaining({ title: 'Updated date' }),
expect.objectContaining({ title: 'Closed date' }),
expect.objectContaining({ title: 'Milestone due date' }),
expect.objectContaining({ title: 'Due date' }),
expect.objectContaining({ title: 'Popularity' }),
expect.objectContaining({ title: 'Title' }),
expect.objectContaining({ title: 'Start date' }),
expect.objectContaining({ title: 'Health' }),
expect.objectContaining({ title: 'Blocking' }),
]);
});
});
});
});
describe('pagination controls', () => {
describe.each`
description | pageInfo | exists

View File

@ -9,8 +9,42 @@ RSpec.describe Projects::ImportExport::RelationExportWorker, type: :worker, feat
it_behaves_like 'an idempotent worker'
shared_examples 'marks relation export failed' do
let(:error_message) { 'Error message' }
let(:exception) { nil }
it 'does not call service, sets relation export status to `failed`, and logs error (exception too if present)' do
expect(Projects::ImportExport::RelationExportService).not_to receive(:new)
expect_next_instance_of(Gitlab::Export::Logger) do |logger|
expect(logger).to receive(:error).with(
hash_including(
message: 'Project relation export failed',
export_error: error_message
)
)
end
if exception.present?
expect_next_instance_of(Gitlab::ExceptionLogFormatter) do |formatter|
expect(formatter).to receive(:format!).with(
exception,
hash_including(
message: 'Project relation export failed',
export_error: error_message
)
)
end
end
worker
expect(project_relation_export.reload.failed?).to eq(true)
end
end
describe '#perform' do
subject(:worker) { described_class.new }
subject(:worker) { described_class.new.perform(*job_args) }
context 'when relation export has initial status `queued`' do
it 'exports the relation' do
@ -18,7 +52,7 @@ RSpec.describe Projects::ImportExport::RelationExportWorker, type: :worker, feat
expect(service).to receive(:execute)
end
worker.perform(*job_args)
worker
end
end
@ -30,7 +64,7 @@ RSpec.describe Projects::ImportExport::RelationExportWorker, type: :worker, feat
expect(service).to receive(:execute)
end
worker.perform(*job_args)
worker
expect(project_relation_export.reload.queued?).to eq(true)
end
@ -42,31 +76,27 @@ RSpec.describe Projects::ImportExport::RelationExportWorker, type: :worker, feat
it 'does not export the relation' do
expect(Projects::ImportExport::RelationExportService).not_to receive(:new)
worker.perform(*job_args)
worker
end
end
context 'when importing user is banned' do
let(:user) { create(:user, :banned) }
it_behaves_like 'marks relation export failed' do
let(:error_message) { "User #{user.id} is banned" }
end
end
end
describe '.sidekiq_retries_exhausted' do
let(:job) { { 'args' => job_args, 'error_message' => 'Error message' } }
let(:job) { { 'args' => job_args, 'error_message' => 'Sidekiq error message' } }
let(:exception) { StandardError.new('Sidekiq error occurred') }
it 'sets relation export status to `failed`' do
described_class.sidekiq_retries_exhausted_block.call(job)
subject(:worker) { described_class.sidekiq_retries_exhausted_block.call(job) }
expect(project_relation_export.reload.failed?).to eq(true)
end
it 'logs the error message' do
expect_next_instance_of(Gitlab::Export::Logger) do |logger|
expect(logger).to receive(:error).with(
hash_including(
message: 'Project relation export failed',
export_error: 'Error message'
)
)
end
described_class.sidekiq_retries_exhausted_block.call(job)
it_behaves_like 'marks relation export failed' do
let(:error_message) { 'Sidekiq error message' }
end
end
end

View File

@ -1346,7 +1346,7 @@
"@floating-ui/core" "^1.7.0"
"@floating-ui/utils" "^0.2.9"
"@floating-ui/dom@^1.0.0", "@floating-ui/dom@^1.7.1":
"@floating-ui/dom@1.7.1", "@floating-ui/dom@^1.0.0", "@floating-ui/dom@^1.7.1":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.1.tgz#76a4e3cbf7a08edf40c34711cf64e0cc8053d912"
integrity sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==
@ -1390,12 +1390,12 @@
core-js "^3.29.1"
mitt "^3.0.1"
"@gitlab/duo-ui@^8.18.0":
version "8.18.0"
resolved "https://registry.yarnpkg.com/@gitlab/duo-ui/-/duo-ui-8.18.0.tgz#ced9368e5f069cb162bb14a9602b45bf9bed6de9"
integrity sha512-FFmQJ8O3I9xoQgALjCfsf4Xy04GQLqEycpc0jkQgUzBuml49Van8Rh3Ig/ZnB6QR40Y+dMDteBE0g3+CpcqlDw==
"@gitlab/duo-ui@^8.22.0":
version "8.22.0"
resolved "https://registry.yarnpkg.com/@gitlab/duo-ui/-/duo-ui-8.22.0.tgz#d7e079df4fdbdd0b7997926392cf3207d6ba20fa"
integrity sha512-q7tsA1PvVh0cszI44TjxTmpS657YqWGuig2s3XUu5q7VQOPr0mnZhMIAFl8VvGK8mLUWk4JUm6mdy0Os+K1QUQ==
dependencies:
"@floating-ui/dom" "1.7.0"
"@floating-ui/dom" "1.7.1"
echarts "^5.3.2"
iframe-resizer "^4.3.2"
lodash "^4.17.20"