Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
5c624e3be3
commit
46df8e869e
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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] } }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
0233515b55c32b5ebcb73dfec6aa362f107a10538484cac3ee0e36833175c2ef
|
||||
|
|
@ -0,0 +1 @@
|
|||
a5521a381e16c988f130e632991eb22eadb7a5f500a4cc429fedf72ddfce9f53
|
||||
|
|
@ -0,0 +1 @@
|
|||
81c5ba0591c9e439013df34658b86dfbbc635c667b1928652bca0cbaffb82ba4
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 >}}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 >}}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 > 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>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
12
yarn.lock
12
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue