Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-06-04 06:19:26 +00:00
parent afe697c0cd
commit 8ba8b01b4e
87 changed files with 816 additions and 447 deletions

View File

@ -40,7 +40,6 @@ Gitlab/FeatureFlagWithoutActor:
- 'app/models/ci/runner.rb'
- 'app/models/ci/secure_file.rb'
- 'app/models/clusters/instance.rb'
- 'app/models/concerns/ci/partitionable/organizer.rb'
- 'app/models/concerns/counter_attribute.rb'
- 'app/models/concerns/protected_ref_access.rb'
- 'app/models/concerns/reset_on_column_errors.rb'

View File

@ -251,11 +251,7 @@ Layout/LineEndStringConcatenationIndentation:
- 'ee/app/workers/requirements_management/process_requirements_reports_worker.rb'
- 'ee/bin/custom-ability'
- 'ee/db/fixtures/development/90_productivity_analytics.rb'
- 'ee/elastic/migrate/20230405500000_backfill_wiki_permissions_in_main_index.rb'
- 'ee/elastic/migrate/20230415500000_migrate_wikis_to_separate_index.rb'
- 'ee/elastic/migrate/20230503064300_backfill_project_permissions_in_blobs_using_permutations.rb'
- 'ee/elastic/migrate/20230518064300_backfill_project_permissions_in_blobs.rb'
- 'ee/elastic/migrate/20230530500000_migrate_projects_to_separate_index.rb'
- 'ee/lib/api/geo_sites.rb'
- 'ee/lib/api/iterations.rb'
- 'ee/lib/api/protected_environments.rb'

View File

@ -1680,10 +1680,7 @@ Style/InlineDisableAnnotation:
- 'ee/db/geo/post_migrate/20231023230850_drop_project_registry.rb'
- 'ee/db/seeds/data_seeder/data_seeder.rb'
- 'ee/db/seeds/shared/dora_metrics.rb'
- 'ee/elastic/migrate/20230325200700_backfill_hashed_root_namespace_id_to_commits.rb'
- 'ee/elastic/migrate/20230405500000_backfill_wiki_permissions_in_main_index.rb'
- 'ee/elastic/migrate/20230518064300_backfill_project_permissions_in_blobs.rb'
- 'ee/elastic/migrate/20230519500012_reindex_wikis_to_fix_permissions_and_traversal_ids.rb'
- 'ee/elastic/migrate/20230702000000_backfill_existing_group_wiki.rb'
- 'ee/elastic/migrate/20230703112233_reindex_commits_to_fix_permissions.rb'
- 'ee/elastic/migrate/20230720000000_reindex_wikis_to_fix_routing.rb'

View File

@ -109,7 +109,9 @@ export default {
return Boolean(this.features?.groupLevelAnalyticsDashboard && this.groupPath);
},
dashboardsPath() {
return this.showLinkToDashboard ? generateValueStreamsDashboardLink(this.groupPath) : null;
return this.showLinkToDashboard
? generateValueStreamsDashboardLink(this.namespace.fullPath)
: null;
},
query() {
return {

View File

@ -27,17 +27,17 @@ const boardDefaults = {
export default {
i18n: {
[formType.new]: { title: s__('Board|Create new board'), btnText: s__('Board|Create board') },
[formType.delete]: { title: s__('Board|Delete board'), btnText: __('Delete') },
[formType.edit]: { title: s__('Board|Configure board'), btnText: __('Save changes') },
scopeModalTitle: s__('Board|Board configuration'),
[formType.new]: { title: s__('Boards|Create new board'), btnText: s__('Boards|Create board') },
[formType.delete]: { title: s__('Boards|Delete board'), btnText: __('Delete') },
[formType.edit]: { title: s__('Boards|Configure board'), btnText: __('Save changes') },
scopeModalTitle: s__('Boards|Board configuration'),
cancelButtonText: __('Cancel'),
deleteButtonText: s__('Board|Delete board'),
deleteErrorMessage: s__('Board|Failed to delete board. Please try again.'),
deleteButtonText: s__('Boards|Delete board'),
deleteErrorMessage: s__('Boards|Failed to delete board. Please try again.'),
saveErrorMessage: __('Unable to save your changes. Please try again.'),
deleteConfirmationMessage: s__('Board|Are you sure you want to delete this board?'),
deleteConfirmationMessage: s__('Boards|Are you sure you want to delete this board?'),
titleFieldLabel: __('Title'),
titleFieldPlaceholder: s__('Board|Enter board name'),
titleFieldPlaceholder: s__('Boards|Enter board name'),
},
components: {
BoardScope: () => import('ee_component/boards/components/board_scope.vue'),

View File

@ -25,10 +25,10 @@ export default {
name: 'BoardsSelector',
i18n: {
fetchBoardsError: s__('Boards|An error occurred while fetching boards. Please try again.'),
headerText: s__('IssueBoards|Switch board'),
noResultsText: s__('IssueBoards|No matching boards found'),
headerText: s__('Boards|Switch board'),
noResultsText: s__('Boards|No matching boards found'),
hiddenBoardsText: s__(
'IssueBoards|Some of your boards are hidden, add a license to see them again.',
'Boards|Some of your boards are hidden, add a license to see them again.',
),
},
components: {
@ -81,7 +81,7 @@ export default {
computed: {
boardName() {
return this.board?.name || s__('IssueBoards|Select board');
return this.board?.name || s__('Boards|Select board');
},
boardId() {
return getIdFromGraphQLId(this.board.id) || '';
@ -283,9 +283,7 @@ export default {
<template #footer>
<div v-if="hasMissingBoards" class="gl-border-t gl-font-sm gl-px-4 gl-pt-4 gl-pb-3">
{{
s__('IssueBoards|Some of your boards are hidden, add a license to see them again.')
}}
{{ s__('Boards|Some of your boards are hidden, add a license to see them again.') }}
</div>
<div v-if="canAdminBoard" class="gl-border-t gl-py-2 gl-px-2">
<gl-button
@ -300,7 +298,7 @@ export default {
data-track-property="dropdown"
@click="$emit('showBoardModal', $options.formType.new)"
>
{{ s__('IssueBoards|Create new board') }}
{{ s__('Boards|Create new board') }}
</gl-button>
</div>
</template>

View File

@ -16,7 +16,7 @@ export default {
inject: ['canAdminList'],
computed: {
buttonText() {
return this.canAdminList ? s__('Board|Configure board') : s__('Board|Board configuration');
return this.canAdminList ? s__('Boards|Configure board') : s__('Boards|Board configuration');
},
},
methods: {

View File

@ -8,7 +8,7 @@ export const TYPE_SNIPPET = 'snippet';
export const BULK_IMPORT_STATIC_ITEMS = {
badges: __('Badge'),
boards: s__('IssueBoards|Board'),
boards: s__('Boards|Board'),
epics: __('Epic'),
issues: __('Issue'),
labels: __('Label'),

View File

@ -1,20 +1,48 @@
<script>
import epicEmptyStateSvg from '@gitlab/svgs/dist/illustrations/empty-state/empty-epic-md.svg';
import issuesEmptyStateSvg from '@gitlab/svgs/dist/illustrations/empty-state/empty-issues-md.svg';
import { GlButton, GlEmptyState } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlButton,
GlEmptyState,
},
inject: ['emptyStateSvgPath', 'newIssuePath', 'showNewIssueLink'],
inject: {
newIssuePath: {
default: false,
},
showNewIssueLink: {
default: false,
},
},
props: {
hasSearch: {
type: Boolean,
required: true,
required: false,
default: false,
},
isEpic: {
type: Boolean,
required: false,
default: false,
},
isOpenTab: {
type: Boolean,
required: true,
required: false,
default: true,
},
},
computed: {
closedTabTitle() {
return this.isEpic ? __('There are no closed epics') : __('There are no closed issues');
},
openTabTitle() {
return this.isEpic ? __('There are no open epics') : __('There are no open issues');
},
svgPath() {
return this.isEpic ? epicEmptyStateSvg : issuesEmptyStateSvg;
},
},
};
@ -25,37 +53,37 @@ export default {
v-if="hasSearch"
:description="__('To widen your search, change or remove filters above')"
:title="__('Sorry, your filter produced no results')"
:svg-path="emptyStateSvgPath"
:svg-height="150"
:svg-path="svgPath"
data-testid="issuable-empty-state"
>
<template #actions>
<gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
{{ __('New issue') }}
</gl-button>
<slot name="new-issue-button">
<gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
{{ __('New issue') }}
</gl-button>
</slot>
</template>
</gl-empty-state>
<gl-empty-state
v-else-if="isOpenTab"
:description="__('To keep this project going, create a new issue')"
:title="__('There are no open issues')"
:svg-path="emptyStateSvgPath"
:svg-height="null"
:title="openTabTitle"
:svg-path="svgPath"
data-testid="issuable-empty-state"
>
<template #actions>
<gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
{{ __('New issue') }}
</gl-button>
<slot name="new-issue-button">
<gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
{{ __('New issue') }}
</gl-button>
</slot>
</template>
</gl-empty-state>
<gl-empty-state
v-else
:title="__('There are no closed issues')"
:svg-path="emptyStateSvgPath"
:svg-height="150"
:title="closedTabTitle"
:svg-path="svgPath"
data-testid="issuable-empty-state"
/>
</template>

View File

@ -1,4 +1,5 @@
<script>
import emptyStateSvg from '@gitlab/svgs/dist/illustrations/empty-state/empty-issues-md.svg';
import { GlButton, GlDisclosureDropdown, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
@ -14,7 +15,9 @@ export default {
'JiraService|%{jiraDocsLinkStart}Enable the Jira integration%{jiraDocsLinkEnd} to view your Jira issues in GitLab.',
),
},
emptyStateSvg,
issuesHelpPagePath: helpPagePath('user/project/issues/index'),
jiraIntegrationPath: helpPagePath('integration/jira/issues', { anchor: 'view-jira-issues' }),
components: {
CsvImportExportButtons,
GlButton,
@ -29,9 +32,7 @@ export default {
mixins: [hasNewIssueDropdown()],
inject: [
'canCreateProjects',
'emptyStateSvgPath',
'isSignedIn',
'jiraIntegrationPath',
'newIssuePath',
'newProjectPath',
'showNewIssueLink',
@ -89,7 +90,7 @@ export default {
<div>
<gl-empty-state
:title="__('Use issues to collaborate on ideas, solve problems, and plan work')"
:svg-path="emptyStateSvgPath"
:svg-path="$options.emptyStateSvg"
:svg-height="150"
data-testid="issuable-empty-state"
>
@ -164,7 +165,7 @@ export default {
<gl-sprintf :message="$options.i18n.jiraIntegrationMessage">
<template #jiraDocsLink="{ content }">
<gl-link
:href="jiraIntegrationPath"
:href="$options.jiraIntegrationPath"
:data-track-action="isProject && 'click_jira_int_project_issues_empty_list_page'"
:data-track-label="isProject && 'jira_int_project_issues_empty_list'"
:data-track-experiment="isProject && 'issues_mrs_empty_state'"
@ -185,7 +186,7 @@ export default {
<gl-empty-state
v-else
:title="__('Use issues to collaborate on ideas, solve problems, and plan work')"
:svg-path="emptyStateSvgPath"
:svg-path="$options.emptyStateSvg"
:svg-height="null"
:primary-button-text="__('Register / Sign In')"
:primary-button-link="signInPath"

View File

@ -8,6 +8,7 @@ import GlCardEmptyStateExperiment from './gl_card_empty_state_experiment.vue';
export default {
issuesHelpPagePath: helpPagePath('user/project/issues/index'),
jiraIntegrationPath: helpPagePath('integration/jira/issues', { anchor: 'view-jira-issues' }),
components: {
GlCardEmptyStateExperiment,
GlButton,
@ -19,9 +20,6 @@ export default {
GlModal: GlModalDirective,
},
inject: {
jiraIntegrationPath: {
default: null,
},
newIssuePath: {
default: null,
},
@ -176,7 +174,7 @@ export default {
>
<a
class="gl-text-decoration-none!"
:href="jiraIntegrationPath"
:href="$options.jiraIntegrationPath"
data-testid="empty-state-jira-int-link"
data-track-action="click_jira_int_project_issues_empty_list_page"
data-track-label="jira_int_project_issues_empty_list"

View File

@ -68,7 +68,6 @@ export async function mountIssuesListApp() {
canReadCrmOrganization,
email,
emailsHelpPagePath,
emptyStateSvgPath,
exportCsvPath,
fullPath,
groupPath,
@ -89,7 +88,6 @@ export async function mountIssuesListApp() {
isProject,
isPublicVisibilityRestricted,
isSignedIn,
jiraIntegrationPath,
markdownHelpPath,
maxAttachmentSize,
newIssuePath,
@ -126,7 +124,6 @@ export async function mountIssuesListApp() {
canCreateProjects: parseBoolean(canCreateProjects),
canReadCrmContact: parseBoolean(canReadCrmContact),
canReadCrmOrganization: parseBoolean(canReadCrmOrganization),
emptyStateSvgPath,
fullPath,
projectPath: fullPath,
groupPath,
@ -148,7 +145,6 @@ export async function mountIssuesListApp() {
isProject: parseBoolean(isProject),
isPublicVisibilityRestricted: parseBoolean(isPublicVisibilityRestricted),
isSignedIn: parseBoolean(isSignedIn),
jiraIntegrationPath,
newIssuePath,
newProjectPath,
releasesPath,

View File

@ -0,0 +1,23 @@
import PageHeading from './page_heading.vue';
export default {
component: PageHeading,
title: 'vue_shared/page_heading',
};
const Template = (args, { argTypes }) => ({
components: { PageHeading },
props: Object.keys(argTypes),
template: `
<page-heading v-bind="$props">
<template #actions>
Actions go here
<template>
</page-heading>
`,
});
export const Default = Template.bind({});
Default.args = {
heading: 'Page heading',
};

View File

@ -0,0 +1,28 @@
<script>
export default {
props: {
heading: {
type: String,
required: false,
default: null,
},
},
};
</script>
<template>
<div
class="gl-flex gl-flex-wrap gl-items-center gl-justify-between gl-gap-y-2 gl-gap-x-5 gl-mt-5 gl-mb-3"
>
<h1 class="gl-heading-1 !gl-m-0" data-testid="page-heading">
{{ heading }}
</h1>
<div
v-if="$scopedSlots.actions"
class="gl-flex gl-items-center gl-gap-5"
data-testid="page-heading-actions"
>
<slot name="actions"></slot>
</div>
</div>
</template>

View File

@ -52,6 +52,11 @@ export default {
type: Object,
required: true,
},
groupPath: {
type: String,
required: false,
default: '',
},
},
computed: {
workItemType() {
@ -247,6 +252,7 @@ export default {
:work-item-id="workItem.id"
:work-item-type="workItemType"
:parent="workItemParent"
:group-path="groupPath"
@error="$emit('error', $event)"
/>
</template>

View File

@ -79,7 +79,7 @@ export default {
WorkItemLoading,
},
mixins: [glFeatureFlagMixin()],
inject: ['fullPath', 'isGroup', 'reportAbusePath'],
inject: ['fullPath', 'isGroup', 'reportAbusePath', 'groupPath'],
props: {
isModal: {
type: Boolean,
@ -609,6 +609,7 @@ export default {
<work-item-attributes-wrapper
:full-path="workItemFullPath"
:work-item="workItem"
:group-path="groupPath"
@error="updateError = $event"
/>
</aside>

View File

@ -14,6 +14,7 @@ import {
I18N_WORK_ITEM_ERROR_UPDATING,
sprintfWorkItem,
SUPPORTED_PARENT_TYPE_MAP,
WORK_ITEM_TYPE_VALUE_ISSUE,
} from '../constants';
export default {
@ -57,6 +58,11 @@ export default {
required: false,
default: false,
},
groupPath: {
type: String,
required: false,
default: '',
},
},
data() {
return {
@ -73,6 +79,9 @@ export default {
hasParent() {
return this.parent !== null;
},
isIssue() {
return this.workItemType === WORK_ITEM_TYPE_VALUE_ISSUE;
},
isLoading() {
return this.$apollo.queries.availableWorkItems.loading;
},
@ -105,16 +114,19 @@ export default {
apollo: {
availableWorkItems: {
query() {
return this.isGroup ? groupWorkItemsQuery : projectWorkItemsQuery;
// TODO: Remove the this.isIssue check once issues are migrated to work items
return this.isGroup || this.isIssue ? groupWorkItemsQuery : projectWorkItemsQuery;
},
variables() {
// TODO: Remove the this.isIssue check once issues are migrated to work items
return {
fullPath: this.fullPath,
fullPath: this.isIssue ? this.groupPath : this.fullPath,
searchTerm: this.search,
types: this.parentType,
in: this.search ? 'TITLE' : undefined,
iid: null,
isNumber: false,
includeAncestors: true,
};
},
skip() {

View File

@ -322,6 +322,7 @@ export const SUPPORTED_PARENT_TYPE_MAP = {
[WORK_ITEM_TYPE_VALUE_KEY_RESULT]: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
[WORK_ITEM_TYPE_VALUE_TASK]: [WORK_ITEM_TYPE_ENUM_ISSUE],
[WORK_ITEM_TYPE_VALUE_EPIC]: [WORK_ITEM_TYPE_ENUM_EPIC],
[WORK_ITEM_TYPE_VALUE_ISSUE]: [WORK_ITEM_TYPE_ENUM_EPIC],
};
export const LINKED_ITEMS_ANCHOR = 'linkeditems';

View File

@ -3,10 +3,11 @@ query groupWorkItems(
$fullPath: ID!
$types: [IssueType!]
$in: [IssuableSearchableField!]
$includeAncestors: Boolean = false
) {
workspace: group(fullPath: $fullPath) {
id
workItems(search: $searchTerm, types: $types, in: $in) {
workItems(search: $searchTerm, types: $types, in: $in, includeAncestors: $includeAncestors) {
nodes {
id
iid

View File

@ -25,6 +25,7 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType } = {}) => {
const {
fullPath,
groupPath,
hasIssueWeightsFeature,
iid,
issuesListPath,
@ -57,6 +58,7 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType } = {}) => {
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
newCommentTemplatePaths: JSON.parse(newCommentTemplatePaths),
reportAbusePath,
groupPath,
},
mounted() {
performanceMarkAndMeasure({

View File

@ -1,5 +1,5 @@
<script>
import { GlFilteredSearchToken } from '@gitlab/ui';
import { GlFilteredSearchToken, GlLoadingIcon } from '@gitlab/ui';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
@ -34,15 +34,25 @@ export default {
issuableListTabs,
sortOptions,
components: {
GlLoadingIcon,
IssuableList,
IssueCardStatistics,
IssueCardTimeInfo,
},
inject: ['fullPath', 'initialSort', 'isSignedIn', 'workItemType'],
props: {
eeCreatedWorkItemsCount: {
type: Number,
required: false,
default: 0,
},
},
data() {
return {
error: undefined,
filterTokens: [],
hasAnyIssues: false,
isInitialAllCountSet: false,
pageInfo: {},
pageParams: getInitialPageParams(),
sortKey: deriveSortKey({ sort: this.initialSort, sortMap: urlSortParams }),
@ -77,6 +87,11 @@ export default {
[STATUS_CLOSED]: closed,
[STATUS_ALL]: all,
};
if (!this.isInitialAllCountSet) {
this.hasAnyIssues = Boolean(all);
this.isInitialAllCountSet = true;
}
},
error(error) {
this.error = s__(
@ -90,6 +105,12 @@ export default {
apiFilterParams() {
return convertToApiParams(this.filterTokens);
},
hasSearch() {
return Boolean(this.searchQuery);
},
isOpenTab() {
return this.state === STATUS_OPEN;
},
searchQuery() {
return convertToSearchQuery(this.filterTokens);
},
@ -137,6 +158,15 @@ export default {
return this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage;
},
},
watch: {
eeCreatedWorkItemsCount() {
// Only reset isInitialAllCountSet when there's no issues to minimize unmounting IssuableList
if (!this.hasAnyIssues) {
this.isInitialAllCountSet = false;
}
this.$apollo.queries.workItems.refetch();
},
},
methods: {
getStatus(issue) {
return issue.state === STATE_CLOSED ? __('Closed') : undefined;
@ -199,7 +229,10 @@ export default {
</script>
<template>
<gl-loading-icon v-if="!isInitialAllCountSet && !error" class="gl-mt-5" size="lg" />
<issuable-list
v-else-if="hasAnyIssues || error"
:current-tab="state"
:error="error"
:has-next-page="pageInfo.hasNextPage"
@ -239,8 +272,16 @@ export default {
<issue-card-statistics :issue="issuable" />
</template>
<template #empty-state>
<slot name="list-empty-state" :has-search="hasSearch" :is-open-tab="isOpenTab"></slot>
</template>
<template #list-body>
<slot name="list-body"></slot>
</template>
</issuable-list>
<div v-else>
<slot name="page-empty-state"></slot>
</div>
</template>

View File

@ -20,6 +20,7 @@ export const mountWorkItemsListApp = () => {
hasIssueWeightsFeature,
initialSort,
isSignedIn,
showNewIssueLink,
workItemType,
} = el.dataset;
@ -37,6 +38,7 @@ export const mountWorkItemsListApp = () => {
initialSort,
isSignedIn: parseBoolean(isSignedIn),
isGroup: true,
showNewIssueLink: parseBoolean(showNewIssueLink),
workItemType,
},
render: (createComponent) => createComponent(WorkItemsListApp),

View File

@ -0,0 +1,7 @@
.gl-flex.gl-flex-wrap.gl-items-center.gl-justify-between.gl-gap-y-2.gl-gap-x-5.gl-mt-5.gl-mb-3
%h1.gl-heading-1{ class: '!gl-m-0', data: { testid: 'page-heading' } }
= heading || @heading
- if actions?
.gl-flex.gl-items-center.gl-gap-5{ data: { testid: 'page-heading-actions' } }
= actions

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module Layouts
class PageHeadingComponent < ViewComponent::Base
# @param [String] heading
def initialize(heading)
@heading = heading
end
renders_one :heading
renders_one :actions
end
end

View File

@ -13,13 +13,18 @@ module Mutations
required: true,
description: 'ID of the Pages Deployment.'
field :pages_deployment, Types::PagesDeploymentType,
null: false,
description: 'Deleted Pages Deployment.'
def resolve(id:)
deployment = authorized_find!(id: id)
deployment.deactivate
{
errors: errors_on_object(deployment)
errors: errors_on_object(deployment),
pages_deployment: deployment
}
end
end

View File

@ -135,14 +135,12 @@ module IssuesHelper
{
autocomplete_award_emojis_path: autocomplete_award_emojis_path,
calendar_path: url_for(safe_params.merge(calendar_url_options)),
empty_state_svg_path: image_path('illustrations/empty-state/empty-service-desk-md.svg'),
full_path: namespace.full_path,
initial_sort: current_user&.user_preference&.issues_sort,
is_issue_repositioning_disabled: issue_repositioning_disabled?.to_s,
is_public_visibility_restricted:
Gitlab::CurrentSettings.restricted_visibility_levels&.include?(Gitlab::VisibilityLevel::PUBLIC).to_s,
is_signed_in: current_user.present?.to_s,
jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'),
rss_path: url_for(safe_params.merge(rss_url_options)),
sign_in_path: new_user_session_path,
has_issue_date_filter_feature: has_issue_date_filter_feature?(namespace, current_user).to_s

View File

@ -6,6 +6,7 @@ module WorkItemsHelper
{
full_path: resource_parent.full_path,
group_path: group&.full_path,
issues_list_path:
resource_parent.is_a?(Group) ? issues_group_path(resource_parent) : project_issues_path(resource_parent),
register_path: new_user_registration_path(redirect_to_referer: 'yes'),
@ -20,7 +21,8 @@ module WorkItemsHelper
{
full_path: group.full_path,
initial_sort: current_user&.user_preference&.issues_sort,
is_signed_in: current_user.present?.to_s
is_signed_in: current_user.present?.to_s,
show_new_issue_link: can?(current_user, :create_work_item, group).to_s
}
end
end

View File

@ -48,6 +48,14 @@ module Ci
.order(id: :asc)
.first
end
def provisioning(partition_id)
Ci::Partition
.with_status(:preparing)
.id_after(partition_id)
.order(id: :asc)
.first
end
end
def above_threshold?(threshold)

View File

@ -576,7 +576,9 @@ module Ci
def self.current_partition_value(project = nil)
Gitlab::SafeRequestStore.fetch(:ci_current_partition_value) do
if Feature.enabled?(:ci_current_partition_value_102, project)
if Feature.enabled?(:ci_partitioning_automation, project)
Ci::Partition.current&.id || NEXT_PARTITION_VALUE
elsif Feature.enabled?(:ci_current_partition_value_102, project)
NEXT_PARTITION_VALUE
elsif Feature.enabled?(:ci_current_partition_value_101, project)
SECOND_PARTITION_VALUE

View File

@ -81,12 +81,19 @@ module Ci
partitioned_by :partition_id,
strategy: :ci_sliding_list,
next_partition_if: ->(latest_partition) do
latest_partition.blank? ||
::Ci::Partitionable::Organizer.create_database_partition?(latest_partition)
latest_partition.blank? || create_database_partition?(latest_partition)
end,
detach_partition_if: proc { false },
analyze_interval: 3.days
end
def create_database_partition?(database_partition)
if Feature.enabled?(:ci_partitioning_automation, :instance)
Ci::Partition.provisioning(database_partition.values.max).present?
else
database_partition.before?(Ci::Pipeline::NEXT_PARTITION_VALUE)
end
end
end
end
end

View File

@ -1,13 +0,0 @@
# frozen_string_literal: true
module Ci
module Partitionable
class Organizer
class << self
def create_database_partition?(database_partition)
database_partition.before?(Ci::Pipeline::NEXT_PARTITION_VALUE)
end
end
end
end
end

View File

@ -11,6 +11,7 @@ module Ci
def execute
return unless Feature.enabled?(:ci_partitioning_first_records, :instance)
return if Ci::Partition.current
setup_default_partitions
end
@ -18,7 +19,8 @@ module Ci
private
def setup_default_partitions
setup_active_partitions && setup_current_partition
setup_active_partitions
setup_current_partition
end
def setup_active_partitions

View File

@ -1,5 +1,4 @@
.page-title-holder.d-flex.gl-align-items-center
%h1.page-title.gl-font-size-h-display= _('Activity')
= render ::Layouts::PageHeadingComponent.new(_('Activity'))
.top-area
= gl_tabs_nav({ class: 'gl-border-b-0', data: { testid: 'dashboard-activity-tabs' } }) do

View File

@ -1,13 +1,11 @@
.page-title-holder.d-flex.gl-align-items-center
%h1.page-title.gl-font-size-h-display= _('Groups')
.page-title-controls.gl-display-flex.gl-align-items-center.gl-gap-5
= render ::Layouts::PageHeadingComponent.new(_('Groups')) do |c|
- c.with_actions do
= link_to _("Explore groups"), explore_groups_path
- if current_user.can_create_group?
= render Pajamas::ButtonComponent.new(href: new_group_path, variant: :confirm, button_options: { data: { testid: "new-group-button" } }) do
= _("New group")
.gl-display-flex.gl-py-3.gl-gap-3
.gl-flex.gl-py-3.gl-gap-3
.gl-w-full
= render 'shared/groups/search_form'
= render 'shared/groups/dropdown'

View File

@ -2,17 +2,15 @@
= content_for :flash_message do
= render 'shared/project_limit'
.page-title-holder.gl-display-flex.gl-align-items-center
%h1.page-title.gl-font-size-h-display= _('Projects')
.page-title-controls.gl-display-flex.gl-align-items-center.gl-gap-5
= render ::Layouts::PageHeadingComponent.new(_('Projects')) do |c|
- c.with_actions do
= link_to _("Explore projects"), starred_explore_projects_path
- if current_user.can_create_project?
= render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm, button_options: { data: { testid: 'new-project-button' } }) do
= _("New project")
.top-area
.scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-flex-basis-0.gl-min-w-0
.scrolling-tabs-container.inner-page-scroll-tabs.gl-grow.gl-basis-0.gl-min-w-0
%button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') }
= sprite_icon('chevron-lg-left', size: 12)
%button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') }

View File

@ -1,7 +1,5 @@
.page-title-holder.d-flex.gl-align-items-center
%h1.page-title.gl-font-size-h-display= _('Snippets')
.page-title-controls.gl-display-flex.gl-align-items-center.gl-gap-5
= render ::Layouts::PageHeadingComponent.new(_('Snippets')) do |c|
- c.with_actions do
= link_to _("Explore snippets"), explore_snippets_path
- if can?(current_user, :create_snippet)
= render Pajamas::ButtonComponent.new(href: new_snippet_path, variant: :confirm, button_options: { title: _("New snippet") }) do

View File

@ -9,11 +9,8 @@
= render_if_exists 'shared/dashboard/saml_reauth_notice',
groups_requiring_saml_reauth: user_groups_requiring_reauth
.page-title-holder.gl-display-flex.gl-align-items-center
%h1.page-title.gl-font-size-h-display= _('Issues')
- if current_user
.page-title-controls
= render 'shared/new_project_item_vue_select'
= render ::Layouts::PageHeadingComponent.new(_('Issues')) do |c|
- c.with_actions do
= render 'shared/new_project_item_vue_select'
.js-issues-dashboard{ data: dashboard_issues_list_data(current_user) }

View File

@ -18,15 +18,12 @@
- if merge_request_dashboard_enabled?(current_user)
#js-merge-request-dashboard{ data: { initial_data: merge_request_dashboard_data.to_json } }
.page-title-holder
%h1.page-title.gl-font-size-h-display= _('Merge Requests')
= render ::Layouts::PageHeadingComponent.new(_('Merge requests'))
= gl_loading_icon(size: 'lg')
- else
.page-title-holder.d-flex.align-items-start.flex-column.flex-sm-row.align-items-sm-center
%h1.page-title.gl-font-size-h-display= title
- if current_user
.page-title-controls.ml-0.mb-3.ml-sm-auto.mb-sm-0
= render ::Layouts::PageHeadingComponent.new(title) do |c|
- c.with_actions do
- if current_user
= render 'shared/new_project_item_vue_select'
.top-area

View File

@ -1,11 +1,9 @@
- page_title _('Milestones')
- add_page_specific_style 'page_bundles/milestone'
.page-title-holder.d-flex.gl-align-items-center
%h1.page-title.gl-font-size-h-display= _('Milestones')
- if current_user
.page-title-controls
= render ::Layouts::PageHeadingComponent.new(_('Milestones')) do |c|
- c.with_actions do
- if current_user
= render 'shared/new_project_item_vue_select'
- if @milestone_states.any? { |name, count| count > 0 }

View File

@ -15,8 +15,7 @@
- show_header = @allowed_todos.any? || user_have_todos
- if show_header
.page-title-holder.d-flex.gl-align-items-center
%h1.page-title.gl-font-size-h-display= _("To-Do List")
= render ::Layouts::PageHeadingComponent.new(_('To-Do List'))
.js-todos-all
- if user_have_todos

View File

@ -3371,7 +3371,7 @@
:tags: []
- :name: merge
:worker_name: MergeWorker
:feature_category: :source_code_management
:feature_category: :code_review_workflow
:has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown

View File

@ -7,7 +7,7 @@ class MergeWorker # rubocop:disable Scalability/IdempotentWorker
sidekiq_options retry: 3
feature_category :source_code_management
feature_category :code_review_workflow
urgency :high
weight 5
loggable_arguments 2

View File

@ -881,22 +881,6 @@ RETURN NEW;
END
$$;
CREATE FUNCTION trigger_8a38ce2327de() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW."group_id" IS NULL THEN
SELECT "group_id"
INTO NEW."group_id"
FROM "epics"
WHERE "epics"."id" = NEW."epic_id";
END IF;
RETURN NEW;
END
$$;
CREATE FUNCTION trigger_84d67ad63e93() RETURNS trigger
LANGUAGE plpgsql
AS $$
@ -913,6 +897,22 @@ RETURN NEW;
END
$$;
CREATE FUNCTION trigger_8a38ce2327de() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW."group_id" IS NULL THEN
SELECT "group_id"
INTO NEW."group_id"
FROM "epics"
WHERE "epics"."id" = NEW."epic_id";
END IF;
RETURN NEW;
END
$$;
CREATE FUNCTION trigger_8ac78f164b2d() RETURNS trigger
LANGUAGE plpgsql
AS $$
@ -30413,10 +30413,10 @@ CREATE TRIGGER trigger_56d49f4ed623 BEFORE INSERT OR UPDATE ON workspace_variabl
CREATE TRIGGER trigger_7a8b08eed782 BEFORE INSERT OR UPDATE ON boards_epic_board_positions FOR EACH ROW EXECUTE FUNCTION trigger_7a8b08eed782();
CREATE TRIGGER trigger_8a38ce2327de BEFORE INSERT OR UPDATE ON boards_epic_user_preferences FOR EACH ROW EXECUTE FUNCTION trigger_8a38ce2327de();
CREATE TRIGGER trigger_84d67ad63e93 BEFORE INSERT OR UPDATE ON wiki_page_slugs FOR EACH ROW EXECUTE FUNCTION trigger_84d67ad63e93();
CREATE TRIGGER trigger_8a38ce2327de BEFORE INSERT OR UPDATE ON boards_epic_user_preferences FOR EACH ROW EXECUTE FUNCTION trigger_8a38ce2327de();
CREATE TRIGGER trigger_8ac78f164b2d BEFORE INSERT OR UPDATE ON design_management_repositories FOR EACH ROW EXECUTE FUNCTION trigger_8ac78f164b2d();
CREATE TRIGGER trigger_8e66b994e8f0 BEFORE INSERT OR UPDATE ON audit_events_streaming_event_type_filters FOR EACH ROW EXECUTE FUNCTION trigger_8e66b994e8f0();

View File

@ -204,7 +204,6 @@ This list of limitations only reflects the latest version of GitLab. If you are
GitLab instances based on our [Reference Architectures](../reference_architectures/index.md), including automation of common daily tasks.
[Epic 1465](https://gitlab.com/groups/gitlab-org/-/epics/1465) proposes to improve Geo installation even more.
- Real-time updates of issues/merge requests (for example, via long polling) doesn't work on the **secondary** site.
- Using Geo secondary sites to accelerate runners is not officially supported. Support for this functionality is planned and can be tracked in [epic 9779](https://gitlab.com/groups/gitlab-org/-/epics/9779). If a replication lag occurs between the primary and secondary site, and the pipeline ref is not available on the secondary site when the job is executed, the job will fail.
- [Selective synchronization](replication/configuration.md#selective-synchronization) only limits what repositories and files are replicated. The entire PostgreSQL data is still replicated. Selective synchronization is not built to accommodate compliance / export control use cases.
- [Pages access control](../../user/project/pages/pages_access_control.md) doesn't work on secondaries. See [GitLab issue #9336](https://gitlab.com/gitlab-org/gitlab/-/issues/9336) for details.
- [Disaster recovery](disaster_recovery/index.md) for deployments that have multiple secondary sites causes downtime due to the need to perform complete re-synchronization and re-configuration of all non-promoted secondaries to follow the new primary site.

View File

@ -3998,6 +3998,7 @@ Input type: `DeletePagesDeploymentInput`
| ---- | ---- | ----------- |
| <a id="mutationdeletepagesdeploymentclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationdeletepagesdeploymenterrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationdeletepagesdeploymentpagesdeployment"></a>`pagesDeployment` | [`PagesDeployment!`](#pagesdeployment) | Deleted Pages Deployment. |
### `Mutation.designManagementDelete`

View File

@ -32,7 +32,7 @@ While the Pipeline Mini Graph primarily functions via REST, we are updating the
## Proposal
To break down implementation, we are taking the following steps:
To break down implementation, we are taking the following steps:
1. Separate the REST version and the GraphQL version of the component into 2 directories called `pipeline_mini_graph` and `legacy_pipeline_mini_graph`. This way, developers can contribute with more ease and we can easily remove the REST version once all apps are using GraphQL.
1. Finish updating the newer component to fully support GraphQL by adding a query for the stage dropdown.

View File

@ -137,6 +137,16 @@ Parent-child relationships form the basis of **hierarchy** in work items. Each w
As types expand, and parent items have their own parent items, the hierarchy capability can grow exponentially.
Currently, following are the allowed Parent-child relationships:
| Type | Can be parent of | Can be child of |
|------------|------------------|------------------|
| Epic | Epic | Epic |
| Issue | Task | Epic |
| Task | None | Issue |
| Objective | Objective | Objective |
| Key result | None | Objective |
### Work Item view
The new frontend view that renders Work Items of any type using global Work Item `id` as an identifier.

View File

@ -6,8 +6,9 @@ info: Any user with at least the Maintainer role can merge updates to this conte
# Query Count Limits
Each controller or API endpoint is allowed to execute up to 100 SQL queries and
in test environments we raise an error when this threshold is exceeded.
Each controller, API endpoint and Sidekide worker is allowed to execute up to
100 SQL queries and in test environments we raise an error when this threshold
is exceeded.
## Solving Failing Tests
@ -20,9 +21,7 @@ solutions to this problem:
You should only resort to disabling query limits when an existing controller or endpoint
is to blame as in this case reducing the number of SQL queries can take a lot of
effort. Newly added controllers and endpoints are not allowed to execute more
than 100 SQL queries and no exceptions are made for this rule. _If_ a large
number of SQL queries is necessary to perform certain work it's best to have
this work performed by Sidekiq instead of doing this directly in a web request.
than 100 SQL queries and no exceptions are made for this rule.
## Disable query limiting
@ -68,3 +67,12 @@ get '/projects/:id/foo' do
# ...
end
```
For Sidekiq workers, you will need to add the allowlist directly as well:
```ruby
def perform(args)
Gitlab::QueryLimiting.disable!('...')
# ...
end

View File

@ -211,6 +211,21 @@ Layout components can be used to create common layout patterns used in GitLab.
### Available components
#### Page heading
A standard page header with a page title and optional actions.
**Example:**
```haml
= render ::Layouts::PageHeadingComponent.new(_('Page title')) do |c|
- c.with_actions do
= buttons
```
For the full list of options, see its
[source](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/components/layouts/page_heading_component.rb).
#### Horizontal section
Many of the settings pages use a layout where the title and description are on the left and the settings fields are on the right. The `Layouts::HorizontalSectionComponent` can be used to create this layout.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@ -53,6 +53,19 @@ Prerequisites:
1. Expand **Secret Detection**.
1. Select the **Allow secret push protection** checkbox.
## Coverage
Secret push protection checks the content of each commit when it is pushed to GitLab.
However, the following exclusions apply.
Secret push protection does not check a file in a commit when:
- The file is a binary file.
- The file is larger than 1 MiB.
- The file was renamed, deleted, or moved without changes to the content.
- The content of the file is identical to the content of another file in the source code.
- The file is contained in the initial push that created the repository.
## Resolve a blocked push
When secret push protection blocks a push, you can either:
@ -128,17 +141,3 @@ To skip secret push protection when using any Git client:
For example, you are using the GitLab Web IDE and have several commits that are blocked from being
pushed because one of them contains a secret. To skip secret push protection, edit the latest
commit message and add `[skip secret detection]`, then push the commits.
## Troubleshooting
When working with secret push protection, you might encounter the following issues.
### My file was not analyzed
If your file was not scanned, it could be because:
- The blob was binary.
- The blob was larger than 1 MiB.
- The file was renamed, deleted, or moved.
- The content of the commit was identical to the content of another file already present in the source code.
- The file was introduced when the repository was created.

View File

@ -207,3 +207,8 @@ You can find definitions for each scan type [`gitlab/lib/gitlab/ci/reports/secur
and [`gitlab/ee/lib/gitlab/ci/reports/security/locations`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/ee/lib/gitlab/ci/reports/security/locations).
For instance, for `container_scanning` type the location is defined by Docker image name without tag. However if the image tag contains at least one letter and/or is longer than 8 characters, it isn't considered a duplicate. So, locations `registry.gitlab.com/group-name/project-name/image1:12345019:libcrypto3` and `registry.gitlab.com/group-name/project-name/image1:libcrypto3` are treated as identical while `registry.gitlab.com/group-name/project-name/image1:v19202021:libcrypto3` and `registry.gitlab.com/group-name/project-name/image1:libcrypto3` are considered different.
## Troubleshooting
In some instances of GitLab 16.8 and earlier [dismissed vulnerabilities are sometimes still visible](https://gitlab.com/gitlab-org/gitlab/-/issues/367298).
This can be resolved by upgrading to GitLab 16.9 and later.

View File

@ -419,7 +419,7 @@ Audit event types belong to the following product categories.
| Name | Description | Saved to database | Streamed | Introduced in | Scope |
|:------------|:------------|:------------------|:---------|:--------------|:--------------|
| [`skip_pre_receive_secret_detection`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147855) | Triggered when secret push protection is skipped by the user| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.11](https://gitlab.com/gitlab-org/gitlab/-/issues/441185) | Project |
| [`skip_secret_push_protection`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147855) | Triggered when secret push protection is skipped by the user| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.11](https://gitlab.com/gitlab-org/gitlab/-/issues/441185) | Project |
### Security policy management

View File

@ -169,7 +169,7 @@ Prerequisites:
Prerequisites:
- You must have at least the Developer role for the project or group the project belongs to.
- You must have at least the Maintainer role for the project or group the project belongs to.
Onboarding a GitLab project means preparing it to receive events that are used for product analytics.

View File

@ -105,8 +105,6 @@ You must define the following variables:
Variable names must contain only lowercase letters (`a-z`), numbers (`0-9`), or underscores (`_`).
You can define URL variables directly with the REST API.
The host portion of the URL (such as `webhook.example.com`) must remain valid without using a mask variable.
Otherwise, a `URI is invalid` or `Url is blocked` error occurs.
### Custom headers

View File

@ -542,7 +542,7 @@ The GitLab SSH folder and files must have the following permissions:
- The `authorized_keys` file must have permissions set to `600`.
- The `authorized_keys.lock` file must have permissions set to `644`.
To verify that these permissions are correct, run the following:
To verify that these permissions are correct, run the following:
```shell
stat -c "%a %n" /var/opt/gitlab/.ssh/.
@ -550,11 +550,11 @@ stat -c "%a %n" /var/opt/gitlab/.ssh/.
### Set permissions
If the permissions are wrong, sign in to the application server and run:
If the permissions are wrong, sign in to the application server and run:
```shell
cd /var/opt/gitlab/
chown git:git /var/opt/gitlab/.ssh/
chown git:git /var/opt/gitlab/.ssh/
chmod 700 /var/opt/gitlab/.ssh/
chmod 600 /var/opt/gitlab/.ssh/authorized_keys
chmod 644 /var/opt/gitlab/.ssh/authorized_keys.lock
@ -584,7 +584,7 @@ This indicates that something is wrong with your SSH setup.
- Try to debug the connection by running `ssh -Tv git@example.com`.
Replace `example.com` with your GitLab URL.
- Ensure you followed all the instructions in [Use SSH on Microsoft Windows](#use-ssh-on-microsoft-windows).
- Ensure that you have [Verify GitLab SSH ownership and permissions](#verify-gitlab-ssh-ownership-and-permissions). If you have several hosts ensure that permissions are correct in all hosts.
- Ensure that you have [Verify GitLab SSH ownership and permissions](#verify-gitlab-ssh-ownership-and-permissions). If you have several hosts ensure that permissions are correct in all hosts.
### `Could not resolve hostname` error

View File

@ -81,7 +81,7 @@ module Sidebars
title = if context.is_super_sidebar
context.group.multiple_issue_boards_available? ? s_('Issue boards') : s_('Issue board')
else
context.group.multiple_issue_boards_available? ? s_('IssueBoards|Boards') : s_('IssueBoards|Board')
context.group.multiple_issue_boards_available? ? s_('Boards|Boards') : s_('Boards|Board')
end
::Sidebars::MenuItem.new(

View File

@ -97,7 +97,7 @@ module Sidebars
title = if context.is_super_sidebar
multi_issue_boards? ? s_('Issue boards') : s_('Issue board')
else
multi_issue_boards? ? s_('IssueBoards|Boards') : s_('IssueBoards|Board')
multi_issue_boards? ? s_('Boards|Boards') : s_('Boards|Board')
end
::Sidebars::MenuItem.new(

View File

@ -8859,32 +8859,71 @@ msgstr ""
msgid "Boards|An error occurred while updating the list. Please try again."
msgstr ""
msgid "Boards|Are you sure you want to delete this board?"
msgstr ""
msgid "Boards|Blocked by %{blockedByCount} %{issuableType}"
msgid_plural "Boards|Blocked by %{blockedByCount} %{issuableType}s"
msgstr[0] ""
msgstr[1] ""
msgid "Boards|Board"
msgstr ""
msgid "Boards|Board configuration"
msgstr ""
msgid "Boards|Boards"
msgstr ""
msgid "Boards|Card options"
msgstr ""
msgid "Boards|Collapse"
msgstr ""
msgid "Boards|Configure board"
msgstr ""
msgid "Boards|Create board"
msgstr ""
msgid "Boards|Create new board"
msgstr ""
msgid "Boards|Create new epic"
msgstr ""
msgid "Boards|Create new issue"
msgstr ""
msgid "Boards|Delete board"
msgstr ""
msgid "Boards|Edit list settings"
msgstr ""
msgid "Boards|Enter board name"
msgstr ""
msgid "Boards|Expand"
msgstr ""
msgid "Boards|Failed to delete board. Please try again."
msgstr ""
msgid "Boards|Failed to fetch blocking %{issuableType}s"
msgstr ""
msgid "Boards|Load more epics"
msgstr ""
msgid "Boards|Load more issues"
msgstr ""
msgid "Boards|Loading epics"
msgstr ""
msgid "Boards|Move to end of list"
msgstr ""
@ -8897,45 +8936,24 @@ msgstr ""
msgid "Boards|No cadence matches current iteration filter"
msgstr ""
msgid "Boards|No matching boards found"
msgstr ""
msgid "Boards|Retrieving blocking %{issuableType}s"
msgstr ""
msgid "Boards|Select board"
msgstr ""
msgid "Boards|Some of your boards are hidden, add a license to see them again."
msgstr ""
msgid "Boards|Switch board"
msgstr ""
msgid "Boards|View all blocking %{issuableType}s"
msgstr ""
msgid "Board|Are you sure you want to delete this board?"
msgstr ""
msgid "Board|Board configuration"
msgstr ""
msgid "Board|Configure board"
msgstr ""
msgid "Board|Create board"
msgstr ""
msgid "Board|Create new board"
msgstr ""
msgid "Board|Delete board"
msgstr ""
msgid "Board|Enter board name"
msgstr ""
msgid "Board|Failed to delete board. Please try again."
msgstr ""
msgid "Board|Load more epics"
msgstr ""
msgid "Board|Load more issues"
msgstr ""
msgid "Board|Loading epics"
msgstr ""
msgid "Bold (%{modifierKey}B)"
msgstr ""
@ -28575,27 +28593,6 @@ msgstr ""
msgid "IssueAnalytics|Weight"
msgstr ""
msgid "IssueBoards|Board"
msgstr ""
msgid "IssueBoards|Boards"
msgstr ""
msgid "IssueBoards|Create new board"
msgstr ""
msgid "IssueBoards|No matching boards found"
msgstr ""
msgid "IssueBoards|Select board"
msgstr ""
msgid "IssueBoards|Some of your boards are hidden, add a license to see them again."
msgstr ""
msgid "IssueBoards|Switch board"
msgstr ""
msgid "IssueList|created %{timeAgoString} by %{user}"
msgstr ""
@ -39456,6 +39453,9 @@ msgstr ""
msgid "ProductAnalytics|Add the script to the page and assign the client SDK to window:"
msgstr ""
msgid "ProductAnalytics|Additional permissions required"
msgstr ""
msgid "ProductAnalytics|After your application has been instrumented and data is being collected, you can visualize and monitor behaviors in your %{linkStart}analytics dashboards%{linkEnd}."
msgstr ""
@ -39528,6 +39528,9 @@ msgstr ""
msgid "ProductAnalytics|Contact our sales team"
msgstr ""
msgid "ProductAnalytics|Contact the GitLab administrator or project maintainer to onboard this project with product analytics. %{linkStart}Learn more%{linkEnd}."
msgstr ""
msgid "ProductAnalytics|Continue set up"
msgstr ""

View File

@ -152,7 +152,7 @@
"gettext-parser": "^6.0.0",
"graphql": "^15.7.2",
"graphql-tag": "^2.11.0",
"gridstack": "^10.1.2",
"gridstack": "^10.2.0",
"highlight.js": "^11.8.0",
"immer": "^9.0.15",
"ipaddr.js": "^1.9.1",

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Layouts::PageHeadingComponent, type: :component, feature_category: :shared do
let(:heading) { 'Page heading' }
let(:actions) { 'Page actions go here' }
describe 'slots' do
it 'renders heading' do
render_inline described_class.new(heading)
expect(page).to have_css('h1.gl-heading-1', text: heading)
end
it 'renders actions slot' do
render_inline described_class.new(heading) do |c|
c.with_actions { actions }
end
expect(page).to have_content(actions)
end
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module Layouts
class PageHeadingComponentPreview < ViewComponent::Preview
# @param heading text
# @param actions text
def default(
heading: 'Page heading',
actions: 'Page actions go here'
)
render(::Layouts::PageHeadingComponent.new(heading)) do |c|
c.with_actions { actions }
end
end
end
end

View File

@ -71,6 +71,6 @@ RSpec.describe 'Dashboard shortcuts', :js, feature_category: :shared do
end
def check_page_title(title)
expect(find('.page-title')).to have_content(title)
expect(find_by_testid('page-heading')).to have_content(title)
end
end

View File

@ -27,7 +27,7 @@ RSpec.describe 'Dashboard snippets', :js, feature_category: :source_code_managem
it_behaves_like 'paginated snippets'
it 'shows new snippet button in header' do
parent_element = page.find('.page-title-controls')
parent_element = find_by_testid('page-heading-actions')
expect(parent_element).to have_link('New snippet')
end
end

View File

@ -180,10 +180,10 @@ describe('Value stream analytics component', () => {
});
});
it('renders a link to the value streams dashboard', () => {
it('renders a link to the value streams dashboard using the namespace path', () => {
expect(findOverviewMetrics().props('dashboardsPath')).toBeDefined();
expect(findOverviewMetrics().props('dashboardsPath')).toBe(
'/groups/foo/-/analytics/dashboards/value_streams_dashboard',
'/full/path/to/foo/-/analytics/dashboards/value_streams_dashboard',
);
});
});

View File

@ -11,11 +11,11 @@ describe('EmptyStateWithAnyIssues component', () => {
wrapper = shallowMount(EmptyStateWithAnyIssues, {
propsData: {
hasSearch: true,
isEpic: false,
isOpenTab: true,
...props,
},
provide: {
emptyStateSvgPath: 'empty/state/svg/path',
newIssuePath: 'new/issue/path',
showNewIssueLink: false,
},
@ -23,42 +23,46 @@ describe('EmptyStateWithAnyIssues component', () => {
};
describe('when there is a search (with no results)', () => {
beforeEach(() => {
mountComponent({ hasSearch: true });
});
it('shows empty state', () => {
mountComponent({ hasSearch: true });
expect(findGlEmptyState().props()).toMatchObject({
description: 'To widen your search, change or remove filters above',
title: 'Sorry, your filter produced no results',
svgPath: 'empty/state/svg/path',
});
});
});
describe('when "Open" tab is active', () => {
beforeEach(() => {
mountComponent({ hasSearch: false, isOpenTab: true });
});
it('shows empty state', () => {
expect(findGlEmptyState().props()).toMatchObject({
description: 'To keep this project going, create a new issue',
title: 'There are no open issues',
svgPath: 'empty/state/svg/path',
});
mountComponent({ hasSearch: false, isOpenTab: true });
expect(findGlEmptyState().props('title')).toBe('There are no open issues');
});
});
describe('when "Closed" tab is active', () => {
beforeEach(() => {
it('shows empty state', () => {
mountComponent({ hasSearch: false, isOpenTab: false });
expect(findGlEmptyState().props('title')).toBe('There are no closed issues');
});
});
describe('when epic', () => {
describe('when "Open" tab is active', () => {
it('shows empty state', () => {
mountComponent({ hasSearch: false, isEpic: true, isOpenTab: true });
expect(findGlEmptyState().props('title')).toBe('There are no open epics');
});
});
it('shows empty state', () => {
expect(findGlEmptyState().props()).toMatchObject({
title: 'There are no closed issues',
svgPath: 'empty/state/svg/path',
describe('when "Closed" tab is active', () => {
it('shows empty state', () => {
mountComponent({ hasSearch: false, isEpic: true, isOpenTab: false });
expect(findGlEmptyState().props('title')).toBe('There are no closed epics');
});
});
});

View File

@ -17,10 +17,8 @@ describe('EmptyStateWithoutAnyIssues component', () => {
const defaultProvide = {
canCreateProjects: false,
emptyStateSvgPath: 'empty/state/svg/path',
fullPath: 'full/path',
isSignedIn: true,
jiraIntegrationPath: 'jira/integration/path',
newIssuePath: 'new/issue/path',
newProjectPath: 'new/project/path',
showNewIssueLink: false,
@ -64,10 +62,9 @@ describe('EmptyStateWithoutAnyIssues component', () => {
it('renders empty state', () => {
mountComponent();
expect(findGlEmptyState().props()).toMatchObject({
title: 'Use issues to collaborate on ideas, solve problems, and plan work',
svgPath: defaultProvide.emptyStateSvgPath,
});
expect(findGlEmptyState().props('title')).toBe(
'Use issues to collaborate on ideas, solve problems, and plan work',
);
});
describe('description', () => {
@ -280,7 +277,9 @@ describe('EmptyStateWithoutAnyIssues component', () => {
});
it('renders Jira integration docs link', () => {
expect(findJiraDocsLink().attributes('href')).toBe(defaultProvide.jiraIntegrationPath);
expect(findJiraDocsLink().attributes('href')).toBe(
'/help/integration/jira/issues#view-jira-issues',
);
});
});
@ -308,7 +307,6 @@ describe('EmptyStateWithoutAnyIssues component', () => {
it('renders empty state', () => {
expect(findGlEmptyState().props()).toMatchObject({
title: 'Use issues to collaborate on ideas, solve problems, and plan work',
svgPath: defaultProvide.emptyStateSvgPath,
primaryButtonText: 'Register / Sign In',
primaryButtonLink: defaultProvide.signInPath,
});

View File

@ -112,7 +112,6 @@ describe('CE IssuesListApp component', () => {
canCreateProjects: false,
canReadCrmContact: false,
canReadCrmOrganization: false,
emptyStateSvgPath: 'empty-state.svg',
exportCsvPath: 'export/csv/path',
fullPath: 'path/to/project',
hasAnyIssues: true,
@ -129,7 +128,6 @@ describe('CE IssuesListApp component', () => {
isProject: true,
isPublicVisibilityRestricted: false,
isSignedIn: true,
jiraIntegrationPath: 'jira/integration/path',
newIssuePath: 'new/issue/path',
newProjectPath: 'new/project/path',
releasesPath: 'releases/path',
@ -610,6 +608,7 @@ describe('CE IssuesListApp component', () => {
it('shows EmptyStateWithAnyIssues empty state', () => {
expect(wrapper.findComponent(EmptyStateWithAnyIssues).props()).toEqual({
hasSearch: false,
isEpic: false,
isOpenTab: true,
});
});

View File

@ -0,0 +1,44 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PageHeading from '~/vue_shared/components/page_heading.vue';
describe('Pagination links component', () => {
const template = `
<template #actions>
Actions go here
</template>
`;
describe('Ordered Layout', () => {
let wrapper;
const createWrapper = () => {
wrapper = shallowMountExtended(PageHeading, {
scopedSlots: {
actions: template,
},
propsData: {
heading: 'Page heading',
},
});
};
const heading = () => wrapper.findByTestId('page-heading');
const actions = () => wrapper.findByTestId('page-heading-actions');
beforeEach(() => {
createWrapper();
});
describe('rendering', () => {
it('renders the correct heading', () => {
expect(heading().text()).toBe('Page heading');
expect(heading().classes()).toEqual(['gl-heading-1', '!gl-m-0']);
expect(heading().element.tagName.toLowerCase()).toBe('h1');
});
it('renders its action slot content', () => {
expect(actions().text()).toBe('Actions go here');
});
});
});
});

View File

@ -9,14 +9,7 @@ import WorkItemParent from '~/work_items/components/work_item_parent.vue';
import WorkItemTimeTracking from '~/work_items/components/work_item_time_tracking.vue';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemAttributesWrapper from '~/work_items/components/work_item_attributes_wrapper.vue';
import {
workItemResponseFactory,
taskType,
objectiveType,
keyResultType,
issueType,
epicType,
} from '../mock_data';
import { workItemResponseFactory } from '../mock_data';
describe('WorkItemAttributesWrapper component', () => {
let wrapper;
@ -34,11 +27,13 @@ describe('WorkItemAttributesWrapper component', () => {
const createComponent = ({
workItem = workItemQueryResponse.data.workItem,
workItemsBeta = true,
groupPath = '',
} = {}) => {
wrapper = shallowMount(WorkItemAttributesWrapper, {
propsData: {
fullPath: 'group/project',
workItem,
groupPath,
},
provide: {
hasIssueWeightsFeature: true,
@ -140,26 +135,9 @@ describe('WorkItemAttributesWrapper component', () => {
});
describe('parent widget', () => {
describe.each`
description | workItemType | exists
${'when work item type is task'} | ${taskType} | ${true}
${'when work item type is objective'} | ${objectiveType} | ${true}
${'when work item type is key result'} | ${keyResultType} | ${true}
${'when work item type is issue'} | ${issueType} | ${true}
${'when work item type is epic'} | ${epicType} | ${true}
`('$description', ({ workItemType, exists }) => {
it(`${exists ? 'renders' : 'does not render'} parent component`, async () => {
const response = workItemResponseFactory({ workItemType });
createComponent({ workItem: response.data.workItem });
await waitForPromises();
expect(findWorkItemParent().exists()).toBe(exists);
});
});
it('renders WorkItemParent when workItemsBeta enabled', async () => {
createComponent();
it(`renders parent component with proper data`, async () => {
const response = workItemResponseFactory();
createComponent({ workItem: response.data.workItem });
await waitForPromises();

View File

@ -137,6 +137,7 @@ describe('WorkItemDetail component', () => {
hasIssuableHealthStatusFeature: true,
projectNamespace: 'namespace',
fullPath: 'group/project',
groupPath: 'group',
isGroup,
reportAbusePath: '/report/abuse/path',
},

View File

@ -292,6 +292,7 @@ describe('WorkItemParent component', () => {
isNumber: false,
searchByIid: false,
searchByText: true,
includeAncestors: true,
});
await findCollapsibleListbox().vm.$emit('search', 'Objective 101');
@ -305,6 +306,7 @@ describe('WorkItemParent component', () => {
isNumber: false,
searchByIid: false,
searchByText: true,
includeAncestors: true,
});
await nextTick();

View File

@ -1,3 +1,4 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
import Vue, { nextTick } from 'vue';
@ -33,6 +34,7 @@ jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
jest.mock('~/sentry/sentry_browser_wrapper');
describe('WorkItemsListApp component', () => {
/** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */
let wrapper;
Vue.use(VueApollo);
@ -63,44 +65,62 @@ describe('WorkItemsListApp component', () => {
});
};
it('renders IssuableList component', () => {
it('renders loading icon when initially fetching work items', () => {
mountComponent();
expect(findIssuableList().props()).toMatchObject({
currentTab: STATUS_OPEN,
error: '',
initialSortBy: CREATED_DESC,
issuables: [],
issuablesLoading: true,
namespace: 'work-items',
recentSearchesStorageKey: 'issues',
showWorkItemTypeIcon: true,
sortOptions,
tabs: WorkItemsListApp.issuableListTabs,
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
describe('when work items are fetched', () => {
beforeEach(async () => {
mountComponent();
await waitForPromises();
});
});
it('renders tab counts', async () => {
mountComponent();
await waitForPromises();
expect(cloneDeep(findIssuableList().props('tabCounts'))).toEqual({
all: 3,
closed: 1,
opened: 2,
it('renders IssuableList component', () => {
expect(findIssuableList().props()).toMatchObject({
currentTab: STATUS_OPEN,
error: '',
initialSortBy: CREATED_DESC,
namespace: 'work-items',
recentSearchesStorageKey: 'issues',
showWorkItemTypeIcon: true,
sortOptions,
tabs: WorkItemsListApp.issuableListTabs,
});
});
});
it('renders IssueCardStatistics component', () => {
mountComponent();
it('renders tab counts', () => {
expect(cloneDeep(findIssuableList().props('tabCounts'))).toEqual({
all: 3,
closed: 1,
opened: 2,
});
});
expect(findIssueCardStatistics().exists()).toBe(true);
});
it('renders IssueCardStatistics component', () => {
expect(findIssueCardStatistics().exists()).toBe(true);
});
it('renders IssueCardTimeInfo component', () => {
mountComponent();
it('renders IssueCardTimeInfo component', () => {
expect(findIssueCardTimeInfo().exists()).toBe(true);
});
expect(findIssueCardTimeInfo().exists()).toBe(true);
it('renders work items', () => {
expect(findIssuableList().props('issuables')).toEqual(
groupWorkItemsQueryResponse.data.group.workItems.nodes,
);
});
it('calls query to fetch work items', () => {
expect(defaultQueryHandler).toHaveBeenCalledWith({
fullPath: 'full/path',
sort: CREATED_DESC,
state: STATUS_OPEN,
firstPageSize: 20,
types: [null],
});
});
});
describe('pagination controls', () => {
@ -122,53 +142,30 @@ describe('WorkItemsListApp component', () => {
});
});
it('renders work items', async () => {
mountComponent();
await waitForPromises();
describe('when workItemType is provided', () => {
it('filters work items by workItemType', () => {
const type = 'EPIC';
mountComponent({ provide: { workItemType: type } });
expect(findIssuableList().props('issuables')).toEqual(
groupWorkItemsQueryResponse.data.group.workItems.nodes,
);
});
it('fetches work items', () => {
mountComponent();
expect(defaultQueryHandler).toHaveBeenCalledWith({
fullPath: 'full/path',
sort: CREATED_DESC,
state: STATUS_OPEN,
firstPageSize: 20,
types: [null],
});
});
it('filters work items by workItemType', () => {
const type = 'EPIC';
mountComponent({
provide: {
workItemType: type,
},
});
expect(defaultQueryHandler).toHaveBeenCalledWith({
fullPath: 'full/path',
sort: CREATED_DESC,
state: STATUS_OPEN,
firstPageSize: 20,
types: [type],
expect(defaultQueryHandler).toHaveBeenCalledWith({
fullPath: 'full/path',
sort: CREATED_DESC,
state: STATUS_OPEN,
firstPageSize: 20,
types: [type],
});
});
});
describe('when there is an error fetching work items', () => {
const message = 'Something went wrong when fetching work items. Please try again.';
beforeEach(async () => {
mountComponent({ queryHandler: jest.fn().mockRejectedValue(new Error('ERROR')) });
await waitForPromises();
});
it('renders an error message', () => {
const message = 'Something went wrong when fetching work items. Please try again.';
expect(findIssuableList().props('error')).toBe(message);
expect(Sentry.captureException).toHaveBeenCalledWith(new Error('ERROR'));
});
@ -177,7 +174,22 @@ describe('WorkItemsListApp component', () => {
findIssuableList().vm.$emit('dismiss-alert');
await nextTick();
expect(findIssuableList().props('error')).toBe('');
expect(wrapper.text()).not.toContain(message);
});
});
describe('watcher', () => {
describe('when eeCreatedWorkItemsCount is updated', () => {
it('refetches work items', async () => {
mountComponent();
await waitForPromises();
expect(defaultQueryHandler).toHaveBeenCalledTimes(1);
await wrapper.setProps({ eeCreatedWorkItemsCount: 1 });
expect(defaultQueryHandler).toHaveBeenCalledTimes(2);
});
});
});
@ -189,7 +201,7 @@ describe('WorkItemsListApp component', () => {
avatar_url: 'avatar/url',
};
beforeEach(() => {
beforeEach(async () => {
window.gon = {
current_user_id: mockCurrentUser.id,
current_user_fullname: mockCurrentUser.name,
@ -197,6 +209,7 @@ describe('WorkItemsListApp component', () => {
current_user_avatar_url: mockCurrentUser.avatar_url,
};
mountComponent();
await waitForPromises();
});
it('renders all tokens', () => {
@ -228,6 +241,7 @@ describe('WorkItemsListApp component', () => {
describe('when "filter" event is emitted by IssuableList', () => {
it('fetches filtered work items', async () => {
mountComponent();
await waitForPromises();
findIssuableList().vm.$emit('filter', [
{ type: FILTERED_SEARCH_TERM, value: { data: 'find issues', operator: 'undefined' } },
@ -280,6 +294,7 @@ describe('WorkItemsListApp component', () => {
} else {
mountComponent();
}
await waitForPromises();
findIssuableList().vm.$emit('sort', sortKey);
await waitForPromises();
@ -291,9 +306,10 @@ describe('WorkItemsListApp component', () => {
);
describe('when user is signed in', () => {
it('calls mutation to save sort preference', () => {
it('calls mutation to save sort preference', async () => {
const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
mountComponent({ sortPreferenceMutationResponse: mutationMock });
await waitForPromises();
findIssuableList().vm.$emit('sort', UPDATED_DESC);
@ -305,6 +321,7 @@ describe('WorkItemsListApp component', () => {
.fn()
.mockResolvedValue(setSortPreferenceMutationResponseWithErrors);
mountComponent({ sortPreferenceMutationResponse: mutationMock });
await waitForPromises();
findIssuableList().vm.$emit('sort', UPDATED_DESC);
await waitForPromises();
@ -314,12 +331,13 @@ describe('WorkItemsListApp component', () => {
});
describe('when user is signed out', () => {
it('does not call mutation to save sort preference', () => {
it('does not call mutation to save sort preference', async () => {
const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
mountComponent({
provide: { isSignedIn: false },
sortPreferenceMutationResponse: mutationMock,
});
await waitForPromises();
findIssuableList().vm.$emit('sort', CREATED_DESC);

View File

@ -3970,6 +3970,29 @@ export const groupWorkItemsQueryResponse = {
},
};
export const emptyGroupWorkItemsQueryResponse = {
data: {
group: {
id: 'gid://gitlab/Group/3',
workItemStateCounts: {
all: 0,
closed: 0,
opened: 0,
},
workItems: {
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: 'startCursor',
endCursor: 'endCursor',
__typename: 'PageInfo',
},
nodes: [],
},
},
},
};
export const updateWorkItemMutationResponseFactory = (options) => {
const response = workItemResponseFactory(options);
return {

View File

@ -41,6 +41,7 @@ describe('Work items router', () => {
router,
provide: {
fullPath: 'full-path',
groupPath: '',
isGroup: false,
issuesListPath: 'full-path/-/issues',
hasIssueWeightsFeature: false,

View File

@ -213,7 +213,6 @@ RSpec.describe IssuesHelper, feature_category: :team_planning do
can_import_issues: 'true',
email: current_user&.notification_email_or_default,
emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'),
empty_state_svg_path: '#',
export_csv_path: export_csv_project_issues_path(project),
full_path: project.full_path,
has_any_issues: project_issues(project).exists?.to_s,
@ -224,7 +223,6 @@ RSpec.describe IssuesHelper, feature_category: :team_planning do
is_project: 'true',
is_public_visibility_restricted: Gitlab::CurrentSettings.restricted_visibility_levels ? 'false' : '',
is_signed_in: current_user.present?.to_s,
jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'),
markdown_help_path: help_page_path('user/markdown'),
max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes),
new_issue_path: new_project_issue_path(project),
@ -281,12 +279,10 @@ RSpec.describe IssuesHelper, feature_category: :team_planning do
autocomplete_award_emojis_path: autocomplete_award_emojis_path,
calendar_path: '#',
can_create_projects: 'true',
empty_state_svg_path: '#',
full_path: group.full_path,
has_any_issues: false.to_s,
has_any_projects: true.to_s,
is_signed_in: current_user.present?.to_s,
jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'),
new_project_path: new_project_path(namespace_id: group.id),
rss_path: '#',
sign_in_path: new_user_session_path,

View File

@ -3,6 +3,7 @@
require "spec_helper"
RSpec.describe WorkItemsHelper, feature_category: :team_planning do
include Devise::Test::ControllerHelpers
describe '#work_items_show_data' do
subject(:work_items_show_data) { helper.work_items_show_data(project) }
@ -12,6 +13,7 @@ RSpec.describe WorkItemsHelper, feature_category: :team_planning do
expect(work_items_show_data).to include(
{
full_path: project.full_path,
group_path: nil,
issues_list_path: project_issues_path(project),
register_path: new_user_registration_path(redirect_to_referer: 'yes'),
sign_in_path: user_session_path(redirect_to_referer: 'yes'),
@ -21,6 +23,21 @@ RSpec.describe WorkItemsHelper, feature_category: :team_planning do
}
)
end
context 'when project is under a group' do
let(:group) { build(:group) }
let(:group_project) { build(:project, group: group) }
subject(:work_items_show_data) { helper.work_items_show_data(group_project) }
it 'returns the expected group_path' do
expect(work_items_show_data).to include(
{
group_path: group_project.group.full_path
}
)
end
end
end
describe '#work_items_list_data' do
@ -32,12 +49,14 @@ RSpec.describe WorkItemsHelper, feature_category: :team_planning do
it 'returns expected data' do
allow(helper).to receive(:current_user).and_return(current_user)
allow(helper).to receive(:can?).and_return(true)
expect(work_items_list_data).to include(
{
full_path: group.full_path,
initial_sort: current_user&.user_preference&.issues_sort,
is_signed_in: current_user.present?.to_s
is_signed_in: current_user.present?.to_s,
show_new_issue_link: 'true'
}
)
end

View File

@ -25,7 +25,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics, feature_category: :shar
worker: 'MergeWorker',
urgency: 'high',
external_dependencies: 'no',
feature_category: 'source_code_management',
feature_category: 'code_review_workflow',
boundary: '',
job_status: 'done',
destination_shard_redis: 'main' })
@ -35,7 +35,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics, feature_category: :shar
worker: 'MergeWorker',
urgency: 'high',
external_dependencies: 'no',
feature_category: 'source_code_management',
feature_category: 'code_review_workflow',
boundary: '',
job_status: 'fail',
destination_shard_redis: 'main' })
@ -103,7 +103,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics, feature_category: :shar
{
worker: 'MergeWorker',
urgency: 'high',
feature_category: 'source_code_management',
feature_category: 'code_review_workflow',
external_dependencies: 'no',
queue: 'merge',
destination_shard_redis: 'main'

View File

@ -95,6 +95,26 @@ RSpec.describe Ci::Partition, feature_category: :ci_scaling do
end
end
end
describe '.provisioning' do
subject(:provisioning) { described_class.provisioning(ci_partition.id) }
let!(:next_ci_partition) { create(:ci_partition) }
context 'when one partition is preparing' do
it { is_expected.to eq(next_ci_partition) }
end
context 'when multiple partitions are preparing' do
before do
create_list(:ci_partition, 2)
end
it 'returns the first ci_partition with status preparing' do
expect(provisioning).to eq(next_ci_partition)
end
end
end
end
describe 'state machine' do

View File

@ -5805,29 +5805,53 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
describe '.current_partition_value' do
subject { described_class.current_partition_value }
it { is_expected.to eq(102) }
context 'when not using ci partitioning automation' do
before do
stub_feature_flags(ci_partitioning_automation: false)
end
it 'accepts an optional argument' do
expect(described_class.current_partition_value(build_stubbed(:project))).to eq(102)
it { is_expected.to eq(102) }
it 'accepts an optional argument' do
expect(described_class.current_partition_value(build_stubbed(:project))).to eq(102)
end
it 'returns 100 when the flags are disabled' do
stub_feature_flags(ci_current_partition_value_101: false)
stub_feature_flags(ci_current_partition_value_102: false)
is_expected.to eq(100)
end
it 'returns 101 when the 102 flag is disabled' do
stub_feature_flags(ci_current_partition_value_102: false)
is_expected.to eq(101)
end
it 'returns 102 when the 101 flag is disabled' do
stub_feature_flags(ci_current_partition_value_101: false)
is_expected.to eq(102)
end
end
it 'returns 100 when the flags are disabled' do
stub_feature_flags(ci_current_partition_value_101: false)
stub_feature_flags(ci_current_partition_value_102: false)
context 'when using ci partitioning automation' do
context 'when current ci_partition exists' do
before do
create(:ci_partition, :current)
end
is_expected.to eq(100)
end
it 'return the current partition value' do
expect(subject).to eq(Ci::Partition.current.id)
end
end
it 'returns 101 when the 102 flag is disabled' do
stub_feature_flags(ci_current_partition_value_102: false)
is_expected.to eq(101)
end
it 'returns 102 when the 101 flag is disabled' do
stub_feature_flags(ci_current_partition_value_101: false)
is_expected.to eq(102)
context 'when current ci_partition does not exist' do
it 'return the default initial value' do
expect(subject).to eq(102)
end
end
end
end

View File

@ -1,34 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::Partitionable::Organizer, feature_category: :continuous_integration do
describe '.create_database_partition?' do
subject(:create_database_partition) { described_class.create_database_partition?(database_partition) }
let(:parent_table_name) { 'p_ci_pipeline_variables' }
let(:partition_name) { 'ci_pipeline_variables_102' }
let(:schema) { 'gitlab_partitions_dynamic' }
let(:database_partition) do
Gitlab::Database::Partitioning::MultipleNumericListPartition.new(
parent_table_name,
[Ci::Pipeline::NEXT_PARTITION_VALUE],
partition_name: partition_name,
schema: schema
)
end
context 'when partition size is greater than the current partition size' do
it { is_expected.to eq(false) }
end
context 'when partition size is less than the current partition size' do
before do
allow(database_partition).to receive_message_chain(:values,
:max).and_return(Ci::Pipeline::INITIAL_PARTITION_VALUE)
end
it { is_expected.to eq(true) }
end
end
end

View File

@ -96,38 +96,58 @@ RSpec.describe Ci::Partitionable, feature_category: :continuous_integration do
subject(:value) { partitioning_strategy.next_partition_if.call(active_partition) }
context 'without any existing partitions' do
it { is_expected.to eq(true) }
end
context 'with initial partition attached' do
context 'when not using ci partitioning automation' do
before do
ci_model.connection.execute(<<~SQL)
CREATE TABLE IF NOT EXISTS _test_table_name_100 PARTITION OF _test_table_name FOR VALUES IN (100);
SQL
stub_feature_flags(ci_partitioning_automation: false)
end
it { is_expected.to eq(true) }
end
context 'with an existing partition for partition_id = 101' do
before do
ci_model.connection.execute(<<~SQL)
CREATE TABLE IF NOT EXISTS _test_table_name_101 PARTITION OF _test_table_name FOR VALUES IN (101);
SQL
context 'without any existing partitions' do
it { is_expected.to eq(true) }
end
it { is_expected.to eq(false) }
end
context 'with initial partition attached' do
before do
ci_model.connection.execute(<<~SQL)
CREATE TABLE IF NOT EXISTS _test_table_name_100 PARTITION OF _test_table_name FOR VALUES IN (100);
SQL
end
context 'with an existing partition for partition_id in 100, 101' do
before do
ci_model.connection.execute(<<~SQL)
CREATE TABLE IF NOT EXISTS _test_table_name_101 PARTITION OF _test_table_name FOR VALUES IN (100, 101);
SQL
it { is_expected.to eq(true) }
end
it { is_expected.to eq(false) }
context 'with an existing partition for partition_id = 101' do
before do
ci_model.connection.execute(<<~SQL)
CREATE TABLE IF NOT EXISTS _test_table_name_101 PARTITION OF _test_table_name FOR VALUES IN (101);
SQL
end
it { is_expected.to eq(false) }
end
context 'with an existing partition for partition_id in 100, 101' do
before do
ci_model.connection.execute(<<~SQL)
CREATE TABLE IF NOT EXISTS _test_table_name_101 PARTITION OF _test_table_name FOR VALUES IN (100, 101);
SQL
end
it { is_expected.to eq(false) }
end
end
context 'when using ci partitioning automation' do
context 'when current ci_partition exists' do
before do
create_list(:ci_partition, 2)
end
it { is_expected.to eq(true) }
end
context 'when current ci_partition does not exist' do
it { is_expected.to eq(false) }
end
end
end
end

View File

@ -15,6 +15,11 @@ RSpec.describe 'DeletePagesDeployment mutation', feature_category: :pages do
<<~GRAPHQL
mutation DeletePagesDeployment {
deletePagesDeployment(input: { id: "#{pages_deployment_id}" }) {
pagesDeployment {
id
active
deletedAt
}
errors
}
}
@ -36,6 +41,22 @@ RSpec.describe 'DeletePagesDeployment mutation', feature_category: :pages do
it 'does not throw an error' do
expect(graphql_errors).to be_nil
end
describe 'returned pages deployment', :freeze_time do
let(:returned_pages_deployment) { graphql_data_at(:deletePagesDeployment, :pages_deployment) }
it 'has the correct ID' do
expect(returned_pages_deployment["id"]).to eq(pages_deployment.to_global_id.to_s)
end
it 'has attribute active:false' do
expect(returned_pages_deployment["active"]).to be(false)
end
it 'has deleted_at set to the deletion time' do
expect(returned_pages_deployment["deletedAt"]).to eq(Time.now.utc.iso8601)
end
end
end
describe 'user is not authorized' do

View File

@ -22,6 +22,16 @@ RSpec.describe Ci::Partitions::SetupDefaultService, feature_category: :ci_scalin
end
end
context 'when current ci_partition exists' do
let!(:current_partition) { create(:ci_partition, :current) }
it 'does not set up default values for ci_partitions' do
expect(service).not_to receive(:setup_default_partitions)
execute
end
end
context 'when default ci_partitions do not exist' do
it 'creates the default partitions', :aggregate_failures do
expect { execute }.to change { Ci::Partition.count }.by(3)

View File

@ -21,7 +21,7 @@ RSpec.describe 'gitlab:feature_categories:index', :silence_stdout, feature_categ
)
),
'sidekiq_workers' => a_hash_including(
'source_code_management' => a_collection_including(
'code_review_workflow' => a_collection_including(
klass: 'MergeWorker',
source_location: [
'app/workers/merge_worker.rb',

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe MergeWorker, feature_category: :source_code_management do
RSpec.describe MergeWorker, feature_category: :code_review_workflow do
describe "remove source branch" do
let!(:merge_request) { create(:merge_request, source_branch: "markdown") }
let!(:source_project) { merge_request.source_project }

View File

@ -7670,10 +7670,10 @@ graphql@^15.7.2:
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.7.2.tgz#85ab0eeb83722977151b3feb4d631b5f2ab287ef"
integrity sha512-AnnKk7hFQFmU/2I9YSQf3xw44ctnSFCfp3zE0N6W174gqe9fWG/2rKaKxROK7CcI3XtERpjEKFqts8o319Kf7A==
gridstack@^10.1.2:
version "10.1.2"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.1.2.tgz#58b5ae0057a8aa5e4f6563041c4ca2def3aa4268"
integrity sha512-Nn27XGQ68WtBC513cKQQ4t/dA2uuN/xnNUU50puXEJv6IFk5SzT0Dnsq68GpopO1n0tXUKZKm1Rw7uOUMDz1KQ==
gridstack@^10.2.0:
version "10.2.0"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.2.0.tgz#4ba9c7ee69a730851721a9f5cb33dc55026ded1f"
integrity sha512-svKAOq/dfinpvhe/nnxdyZOOEd9qynXiOPHvL96PALE0yWChWp/6lechnqKwud0tL/rRyAfMJ6Hh/z2fS13pBA==
gzip-size@^6.0.0:
version "6.0.0"