Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
afe697c0cd
commit
8ba8b01b4e
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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') }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ describe('WorkItemDetail component', () => {
|
|||
hasIssuableHealthStatusFeature: true,
|
||||
projectNamespace: 'namespace',
|
||||
fullPath: 'group/project',
|
||||
groupPath: 'group',
|
||||
isGroup,
|
||||
reportAbusePath: '/report/abuse/path',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ describe('Work items router', () => {
|
|||
router,
|
||||
provide: {
|
||||
fullPath: 'full-path',
|
||||
groupPath: '',
|
||||
isGroup: false,
|
||||
issuesListPath: 'full-path/-/issues',
|
||||
hasIssueWeightsFeature: false,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue