Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2024-01-22 15:08:07 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2024-01-22 15:08:07 +0300
commite5c31c104e19a08546b17b34b7f1563cce3f89e6 (patch)
tree4f05401dc288370583328a94c1bbf1f1de9ace64
parent56865fdf95db03cc0ccd01a88d9457ba0a050153 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.checksum20
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/components/app.vue19
-rw-r--r--app/assets/javascripts/organizations/shared/components/projects_view.vue92
-rw-r--r--app/assets/javascripts/organizations/shared/constants.js3
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql15
-rw-r--r--app/assets/javascripts/organizations/shared/utils.js24
-rw-r--r--app/assets/javascripts/organizations/show/components/groups_and_projects.vue24
-rw-r--r--app/assets/javascripts/organizations/show/constants.js2
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue2
-rw-r--r--app/models/application_setting.rb1
-rw-r--r--app/models/application_setting_implementation.rb3
-rw-r--r--app/models/ci/pipeline_variable.rb1
-rw-r--r--app/views/devise/shared/_omniauth_provider_button.haml2
-rw-r--r--app/views/devise/shared/_signup_omniauth_provider_button.haml3
-rw-r--r--db/docs/p_ci_pipeline_variables.yml10
-rw-r--r--db/migrate/20240110092610_add_index_on_project_id_to_web_hooks.rb19
-rw-r--r--db/migrate/20240117081214_add_enable_user_cap_member_promotion_management_to_application_settings.rb9
-rw-r--r--db/post_migrate/20240118125559_convert_ci_pipeline_variables_to_list_partitioning_adds_fk_to_ci_pipelines.rb31
-rw-r--r--db/schema_migrations/202401100926101
-rw-r--r--db/schema_migrations/202401170812141
-rw-r--r--db/schema_migrations/202401181255591
-rw-r--r--db/structure.sql43
-rw-r--r--doc/development/fe_guide/customizable_dashboards.md2
-rw-r--r--doc/subscriptions/gitlab_dedicated/index.md6
-rw-r--r--doc/topics/git/git_rebase.md32
-rw-r--r--doc/user/project/quick_actions.md2
-rw-r--r--lib/gitlab/internal_events.rb4
-rw-r--r--spec/db/schema_spec.rb1
-rw-r--r--spec/features/registrations/oauth_registration_spec.rb2
-rw-r--r--spec/frontend/organizations/groups_and_projects/components/app_spec.js67
-rw-r--r--spec/frontend/organizations/shared/components/projects_view_spec.js144
-rw-r--r--spec/frontend/organizations/shared/utils_spec.js41
-rw-r--r--spec/lib/gitlab/database/sharding_key_spec.rb3
-rw-r--r--spec/lib/gitlab/internal_events_spec.rb2
-rw-r--r--spec/models/application_setting_spec.rb2
-rw-r--r--spec/support/helpers/login_helpers.rb2
38 files changed, 590 insertions, 52 deletions
diff --git a/Gemfile b/Gemfile
index 86648ac67ca..01b0b9b7c86 100644
--- a/Gemfile
+++ b/Gemfile
@@ -269,7 +269,7 @@ gem 'rainbow', '~> 3.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'ruby-progressbar', '~> 1.10' # rubocop:todo Gemfile/MissingFeatureCategory
# Linear-time regex library for untrusted regular expressions
-gem 're2', '2.6.0' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 're2', '2.7.0' # rubocop:todo Gemfile/MissingFeatureCategory
# Misc
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 5d0ea378879..7ad4c2dc349 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -504,16 +504,16 @@
{"name":"rbtrace","version":"0.4.14","platform":"ruby","checksum":"162bbf89cecabfc4f09c869b655f6f3a679c4870ebb7cbdcadf7393a81cc1769"},
{"name":"rbtree","version":"0.4.6","platform":"ruby","checksum":"14eea4469b24fd2472542e5f3eb105d6344c8ccf36f0b56d55fdcfeb4e0f10fc"},
{"name":"rchardet","version":"1.8.0","platform":"ruby","checksum":"693acd5253d5ade81a51940697955f6dd4bb2f0d245bda76a8e23deec70a52c7"},
-{"name":"re2","version":"2.6.0","platform":"aarch64-linux","checksum":"1cb558bdeabe01bb935988f49969613a83681014392c81ed10caa62351fb91d6"},
-{"name":"re2","version":"2.6.0","platform":"arm-linux","checksum":"b6007820d8a7a723d9549e44118a696dd289d8822a324c52d3663d3d1e9479f6"},
-{"name":"re2","version":"2.6.0","platform":"arm64-darwin","checksum":"ba6fda7a29cd16179d5401c1b4917ba204c92e5ca9d25df80d840ed76fca439f"},
-{"name":"re2","version":"2.6.0","platform":"ruby","checksum":"78e13aa6a9ee962b76eb2aa08b6e3246652a1ce3d7b097eb114b13ff4606486a"},
-{"name":"re2","version":"2.6.0","platform":"x64-mingw-ucrt","checksum":"8332068cbb0ec170938bd27c518a298e9d78be53d2e7ea004f4994290559c330"},
-{"name":"re2","version":"2.6.0","platform":"x64-mingw32","checksum":"e9c114c59332fa782e68d6695d2f511eb5eb973e535c55a903de4470501b4cf0"},
-{"name":"re2","version":"2.6.0","platform":"x86-linux","checksum":"0177a4d83268cd125e15acef96d6bde625ab4bb892d04999733104e39155b270"},
-{"name":"re2","version":"2.6.0","platform":"x86-mingw32","checksum":"5b3f1413edc3c0c54df27f6b27f130ed3d64a124df93fdf676f078dd6faf2bfe"},
-{"name":"re2","version":"2.6.0","platform":"x86_64-darwin","checksum":"0f2463816345fd5b6a1e552711c2a3d5f86275e3f72dcfee0d400c89e14842c2"},
-{"name":"re2","version":"2.6.0","platform":"x86_64-linux","checksum":"eabc7877470c5dd08f4ecd12040f6c44400403a9252dddda024a3092f2af1604"},
+{"name":"re2","version":"2.7.0","platform":"aarch64-linux","checksum":"778921298b6e8aba26a6230dd298c9b361b92e45024f81fa6aee788060fa307c"},
+{"name":"re2","version":"2.7.0","platform":"arm-linux","checksum":"d328b5286d83ae265e13b855da8e348a976f80f91b748045b52073a570577954"},
+{"name":"re2","version":"2.7.0","platform":"arm64-darwin","checksum":"7d993f27a1afac4001c539a829e2af211ced62604930c90df32a307cf74cb4a4"},
+{"name":"re2","version":"2.7.0","platform":"ruby","checksum":"319d88d2a8ff32eba45286e58a8d49bd8b59167afa718946165b67fec2ea13dc"},
+{"name":"re2","version":"2.7.0","platform":"x64-mingw-ucrt","checksum":"30ac6e9831f2e2237f5c6f2813f1f708110b7b707e571504aae56083eb26011c"},
+{"name":"re2","version":"2.7.0","platform":"x64-mingw32","checksum":"78c98a7d4d272eedb6b5779c25b55d8f8ddd4ba4b0b6f402616e4717f785841d"},
+{"name":"re2","version":"2.7.0","platform":"x86-linux","checksum":"0c7ba10b907738402577e4a4d7b980fbaae05850f418a4d7061a7ba531a4fe57"},
+{"name":"re2","version":"2.7.0","platform":"x86-mingw32","checksum":"3560795ee796386a50f5c5f002d0a8570995da48599d524cac23d3822b9a1d7f"},
+{"name":"re2","version":"2.7.0","platform":"x86_64-darwin","checksum":"49afef32dbc8b0a0fe978668489bdc3f016c30afb20d0aecb75db0454297c4ff"},
+{"name":"re2","version":"2.7.0","platform":"x86_64-linux","checksum":"be9095c33e9628644fd876c1e6379cbd6a99a1c786d496e087491471f78d53fc"},
{"name":"recaptcha","version":"5.12.3","platform":"ruby","checksum":"37d1894add9e70a54d0c6c7f0ecbeedffbfa7d075acfbd4c509818dfdebdb7ee"},
{"name":"recursive-open-struct","version":"1.1.3","platform":"ruby","checksum":"a3538a72552fcebcd0ada657bdff313641a4a5fbc482c08cfb9a65acb1c9de5a"},
{"name":"redcarpet","version":"3.6.0","platform":"ruby","checksum":"8ad1889c0355ff4c47174af14edd06d62f45a326da1da6e8a121d59bdcd2e9e9"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 4c734a403d2..b24601eed37 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1352,7 +1352,7 @@ GEM
optimist (>= 3.0.0)
rbtree (0.4.6)
rchardet (1.8.0)
- re2 (2.6.0)
+ re2 (2.7.0)
mini_portile2 (~> 2.8.5)
recaptcha (5.12.3)
json
@@ -2044,7 +2044,7 @@ DEPENDENCIES
rails-i18n (~> 7.0)
rainbow (~> 3.0)
rbtrace (~> 0.4)
- re2 (= 2.6.0)
+ re2 (= 2.7.0)
recaptcha (~> 5.12)
redis (~> 4.8.0)
redis-actionpack (~> 5.4.0)
diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
index 6f03783d15c..4c4d04fe1cb 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
+++ b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
@@ -16,6 +16,8 @@ import {
import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '../../constants';
import GroupsView from '../../shared/components/groups_view.vue';
import ProjectsView from '../../shared/components/projects_view.vue';
+import { onPageChange } from '../../shared/utils';
+import { QUERY_PARAM_END_CURSOR, QUERY_PARAM_START_CURSOR } from '../../shared/constants';
import {
DISPLAY_LISTBOX_ITEMS,
SORT_DIRECTION_ASC,
@@ -66,6 +68,12 @@ export default {
sortText() {
return this.activeSortItem.text;
},
+ startCursor() {
+ return this.$route.query[QUERY_PARAM_START_CURSOR] || null;
+ },
+ endCursor() {
+ return this.$route.query[QUERY_PARAM_END_CURSOR] || null;
+ },
filteredSearchValue() {
const tokens = prepareTokens(
urlQueryToFilter(this.$route.query, {
@@ -122,6 +130,9 @@ export default {
}),
});
},
+ onPageChange(pagination) {
+ this.pushQuery(onPageChange({ ...pagination, routeQuery: this.$route.query }));
+ },
},
};
</script>
@@ -166,6 +177,12 @@ export default {
</div>
</div>
</div>
- <component :is="routerView" list-item-class="gl-px-5" />
+ <component
+ :is="routerView"
+ list-item-class="gl-px-5"
+ :start-cursor="startCursor"
+ :end-cursor="endCursor"
+ @page-change="onPageChange"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/organizations/shared/components/projects_view.vue b/app/assets/javascripts/organizations/shared/components/projects_view.vue
index 5feb871740a..4d23021bccc 100644
--- a/app/assets/javascripts/organizations/shared/components/projects_view.vue
+++ b/app/assets/javascripts/organizations/shared/components/projects_view.vue
@@ -1,7 +1,8 @@
<script>
-import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import { GlLoadingIcon, GlEmptyState, GlKeysetPagination } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
+import { DEFAULT_PER_PAGE } from '~/api';
import { createAlert } from '~/alert';
import projectsQuery from '../graphql/queries/projects.query.graphql';
import { formatProjects } from '../utils';
@@ -18,11 +19,14 @@ export default {
),
primaryButtonText: __('New project'),
},
+ prev: __('Prev'),
+ next: __('Next'),
},
components: {
ProjectsList,
GlLoadingIcon,
GlEmptyState,
+ GlKeysetPagination,
},
inject: {
organizationGid: {},
@@ -42,11 +46,48 @@ export default {
required: false,
default: '',
},
+ startCursor: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ endCursor: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ perPage: {
+ type: Number,
+ required: false,
+ default: DEFAULT_PER_PAGE,
+ },
},
data() {
- return {
+ const baseData = {
projects: {},
};
+
+ if (!this.startCursor && !this.endCursor) {
+ return {
+ ...baseData,
+ pagination: {
+ first: this.perPage,
+ after: null,
+ last: null,
+ before: null,
+ },
+ };
+ }
+
+ return {
+ ...baseData,
+ pagination: {
+ first: this.endCursor && this.perPage,
+ after: this.endCursor,
+ last: this.startCursor && this.perPage,
+ before: this.startCursor,
+ },
+ };
},
apollo: {
projects: {
@@ -54,6 +95,7 @@ export default {
variables() {
return {
id: this.organizationGid,
+ ...this.pagination,
};
},
update({
@@ -66,6 +108,13 @@ export default {
pageInfo,
};
},
+ result() {
+ this.$emit('page-change', {
+ endCursor: this.pagination.after,
+ startCursor: this.pagination.before,
+ hasPreviousPage: this.pageInfo.hasPreviousPage,
+ });
+ },
error(error) {
createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
},
@@ -75,6 +124,9 @@ export default {
nodes() {
return this.projects.nodes || [];
},
+ pageInfo() {
+ return this.projects.pageInfo || {};
+ },
isLoading() {
return this.$apollo.queries.projects.loading;
},
@@ -97,16 +149,40 @@ export default {
return baseProps;
},
},
+ methods: {
+ onNext(endCursor) {
+ this.pagination = {
+ first: this.perPage,
+ after: endCursor,
+ last: null,
+ before: null,
+ };
+ },
+ onPrev(startCursor) {
+ this.pagination = {
+ first: null,
+ after: null,
+ last: this.perPage,
+ before: startCursor,
+ };
+ },
+ },
};
</script>
<template>
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
- <projects-list
- v-else-if="nodes.length"
- :projects="nodes"
- show-project-icon
- :list-item-class="listItemClass"
- />
+ <div v-else-if="nodes.length">
+ <projects-list :projects="nodes" show-project-icon :list-item-class="listItemClass" />
+ <div v-if="pageInfo.hasNextPage || pageInfo.hasPreviousPage" class="gl-text-center gl-mt-5">
+ <gl-keyset-pagination
+ v-bind="pageInfo"
+ :prev-text="$options.i18n.prev"
+ :next-text="$options.i18n.next"
+ @prev="onPrev"
+ @next="onNext"
+ />
+ </div>
+ </div>
<gl-empty-state v-else v-bind="emptyStateProps" />
</template>
diff --git a/app/assets/javascripts/organizations/shared/constants.js b/app/assets/javascripts/organizations/shared/constants.js
index c4f4f348ff2..c994efb7809 100644
--- a/app/assets/javascripts/organizations/shared/constants.js
+++ b/app/assets/javascripts/organizations/shared/constants.js
@@ -14,3 +14,6 @@ export const FORM_FIELD_PATH_VALIDATORS = [
(val) => val.length >= 2,
),
];
+
+export const QUERY_PARAM_START_CURSOR = 'start_cursor';
+export const QUERY_PARAM_END_CURSOR = 'end_cursor';
diff --git a/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql b/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql
index caf82ef90ca..f74d1002ab2 100644
--- a/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql
+++ b/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql
@@ -1,7 +1,15 @@
-query getOrganizationProjects($id: OrganizationsOrganizationID!) {
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+
+query getOrganizationProjects(
+ $id: OrganizationsOrganizationID!
+ $first: Int
+ $last: Int
+ $before: String
+ $after: String
+) {
organization(id: $id) {
id
- projects {
+ projects(first: $first, last: $last, before: $before, after: $after) {
nodes {
id
nameWithNamespace
@@ -24,6 +32,9 @@ query getOrganizationProjects($id: OrganizationsOrganizationID!) {
stringValue
}
}
+ pageInfo {
+ ...PageInfo
+ }
}
}
}
diff --git a/app/assets/javascripts/organizations/shared/utils.js b/app/assets/javascripts/organizations/shared/utils.js
index fd172f09ec9..068eb10815e 100644
--- a/app/assets/javascripts/organizations/shared/utils.js
+++ b/app/assets/javascripts/organizations/shared/utils.js
@@ -1,5 +1,6 @@
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
+import { QUERY_PARAM_END_CURSOR, QUERY_PARAM_START_CURSOR } from './constants';
export const formatProjects = (projects) =>
projects.map(
@@ -33,3 +34,26 @@ export const formatGroups = (groups) =>
editPath: `${webUrl}/-/edit`,
availableActions: [ACTION_EDIT, ACTION_DELETE],
}));
+
+export const onPageChange = ({
+ startCursor,
+ endCursor,
+ hasPreviousPage,
+ routeQuery: { start_cursor, end_cursor, ...routeQuery },
+}) => {
+ if (startCursor && hasPreviousPage) {
+ return {
+ ...routeQuery,
+ [QUERY_PARAM_START_CURSOR]: startCursor,
+ };
+ }
+
+ if (endCursor) {
+ return {
+ ...routeQuery,
+ [QUERY_PARAM_END_CURSOR]: endCursor,
+ };
+ }
+
+ return routeQuery;
+};
diff --git a/app/assets/javascripts/organizations/show/components/groups_and_projects.vue b/app/assets/javascripts/organizations/show/components/groups_and_projects.vue
index e8972f3b380..d1437438712 100644
--- a/app/assets/javascripts/organizations/show/components/groups_and_projects.vue
+++ b/app/assets/javascripts/organizations/show/components/groups_and_projects.vue
@@ -4,8 +4,10 @@ import { isEqual } from 'lodash';
import { s__, __ } from '~/locale';
import GroupsView from '../../shared/components/groups_view.vue';
import ProjectsView from '../../shared/components/projects_view.vue';
+import { onPageChange } from '../../shared/utils';
+import { QUERY_PARAM_END_CURSOR, QUERY_PARAM_START_CURSOR } from '../../shared/constants';
import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '../../constants';
-import { FILTER_FREQUENTLY_VISITED } from '../constants';
+import { FILTER_FREQUENTLY_VISITED, GROUPS_AND_PROJECTS_PER_PAGE } from '../constants';
import { buildDisplayListboxItem } from '../utils';
export default {
@@ -28,6 +30,7 @@ export default {
text: s__('Organization|Frequently visited groups'),
}),
],
+ PER_PAGE: GROUPS_AND_PROJECTS_PER_PAGE,
props: {
groupsAndProjectsOrganizationPath: {
type: String,
@@ -44,6 +47,12 @@ export default {
fallbackSelected
);
},
+ startCursor() {
+ return this.$route.query[QUERY_PARAM_START_CURSOR] || null;
+ },
+ endCursor() {
+ return this.$route.query[QUERY_PARAM_END_CURSOR] || null;
+ },
resourceTypeSelected() {
return [RESOURCE_TYPE_PROJECTS, RESOURCE_TYPE_GROUPS].find((resourceType) =>
this.displayListboxSelected.endsWith(resourceType),
@@ -78,6 +87,9 @@ export default {
onDisplayListboxSelect(display) {
this.pushQuery({ display });
},
+ onPageChange(pagination) {
+ this.pushQuery(onPageChange({ ...pagination, routeQuery: this.$route.query }));
+ },
},
};
</script>
@@ -105,6 +117,14 @@ export default {
$options.i18n.viewAll
}}</gl-link>
</div>
- <component :is="routerView" should-show-empty-state-buttons class="gl-mt-5" />
+ <component
+ :is="routerView"
+ should-show-empty-state-buttons
+ class="gl-mt-5"
+ :start-cursor="startCursor"
+ :end-cursor="endCursor"
+ :per-page="$options.PER_PAGE"
+ @page-change="onPageChange"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/organizations/show/constants.js b/app/assets/javascripts/organizations/show/constants.js
index fe29af67f6b..eaaaf86f260 100644
--- a/app/assets/javascripts/organizations/show/constants.js
+++ b/app/assets/javascripts/organizations/show/constants.js
@@ -1 +1,3 @@
export const FILTER_FREQUENTLY_VISITED = 'frequently_visited';
+
+export const GROUPS_AND_PROJECTS_PER_PAGE = 5;
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
index 480549452ad..d16d241f297 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
@@ -241,7 +241,7 @@ export default {
:work-item-id="workItemId"
:work-item-state="workItemState"
:work-item-type="workItemType"
- :has-comment="!!commentText.length"
+ :has-comment="Boolean(commentText.length)"
can-update
@submit-comment="$emit('submitForm', { commentText, isNoteInternal })"
@error="$emit('error', $event)"
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 490029ce0b5..cf140c914b4 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -379,6 +379,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
:can_create_organization,
:allow_project_creation_for_guest_and_below,
:user_defaults_to_private_profile,
+ :enable_member_promotion_management,
allow_nil: false,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index d1899b18a4f..14aab129f72 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -278,7 +278,8 @@ module ApplicationSettingImplementation
gitlab_shell_operation_limit: 600,
project_jobs_api_rate_limit: 600,
security_txt_content: nil,
- allow_project_creation_for_guest_and_below: true
+ allow_project_creation_for_guest_and_below: true,
+ enable_member_promotion_management: false
}.tap do |hsh|
hsh.merge!(non_production_defaults) unless Rails.env.production?
end
diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb
index 4fddb3e053e..d83bb29ff80 100644
--- a/app/models/ci/pipeline_variable.rb
+++ b/app/models/ci/pipeline_variable.rb
@@ -9,6 +9,7 @@ module Ci
belongs_to :pipeline
self.primary_key = :id
+ self.sequence_name = :ci_pipeline_variables_id_seq
partitionable scope: :pipeline
diff --git a/app/views/devise/shared/_omniauth_provider_button.haml b/app/views/devise/shared/_omniauth_provider_button.haml
index c33e2253bb1..40b23832ba7 100644
--- a/app/views/devise/shared/_omniauth_provider_button.haml
+++ b/app/views/devise/shared/_omniauth_provider_button.haml
@@ -1,4 +1,4 @@
-- button_options = { class: local_assigns.fetch(:classes, nil) || nil, data: data, id: id }
+- button_options = { class: local_assigns[:classes], data: data, id: local_assigns[:id] }
= render Pajamas::ButtonComponent.new(href: href, method: :post, form: true, block: true, button_options: button_options) do
- if provider_has_icon?(provider)
diff --git a/app/views/devise/shared/_signup_omniauth_provider_button.haml b/app/views/devise/shared/_signup_omniauth_provider_button.haml
index 9870e90cfff..0c08940f4a8 100644
--- a/app/views/devise/shared/_signup_omniauth_provider_button.haml
+++ b/app/views/devise/shared/_signup_omniauth_provider_button.haml
@@ -2,5 +2,4 @@
href: href,
provider: provider,
classes: 'js-track-omni-auth',
- data: { provider: provider, track_action: "#{provider}_sso", track_label: tracking_label },
- id: "oauth-login-#{provider}"
+ data: { provider: provider, track_action: "#{provider}_sso", track_label: tracking_label }
diff --git a/db/docs/p_ci_pipeline_variables.yml b/db/docs/p_ci_pipeline_variables.yml
new file mode 100644
index 00000000000..38dbf96ac94
--- /dev/null
+++ b/db/docs/p_ci_pipeline_variables.yml
@@ -0,0 +1,10 @@
+---
+table_name: p_ci_pipeline_variables
+classes:
+- Ci::PipelineVariable
+feature_categories:
+- continuous_integration
+description: Routing table for ci_pipeline_variables
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/141270
+milestone: '16.9'
+gitlab_schema: gitlab_ci
diff --git a/db/migrate/20240110092610_add_index_on_project_id_to_web_hooks.rb b/db/migrate/20240110092610_add_index_on_project_id_to_web_hooks.rb
new file mode 100644
index 00000000000..95c64caa1ce
--- /dev/null
+++ b/db/migrate/20240110092610_add_index_on_project_id_to_web_hooks.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddIndexOnProjectIdToWebHooks < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+
+ milestone '16.9'
+
+ TABLE_NAME = :web_hooks
+ INDEX_NAME = 'index_web_hooks_on_project_id_and_id'
+ CLAUSE = "((type)::text = 'ProjectHook'::text)"
+
+ def up
+ add_concurrent_index TABLE_NAME, [:project_id, :id], name: INDEX_NAME, where: CLAUSE
+ end
+
+ def down
+ remove_concurrent_index_by_name TABLE_NAME, INDEX_NAME
+ end
+end
diff --git a/db/migrate/20240117081214_add_enable_user_cap_member_promotion_management_to_application_settings.rb b/db/migrate/20240117081214_add_enable_user_cap_member_promotion_management_to_application_settings.rb
new file mode 100644
index 00000000000..2b3c13b1600
--- /dev/null
+++ b/db/migrate/20240117081214_add_enable_user_cap_member_promotion_management_to_application_settings.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddEnableUserCapMemberPromotionManagementToApplicationSettings < Gitlab::Database::Migration[2.2]
+ milestone '16.9'
+
+ def change
+ add_column(:application_settings, :enable_member_promotion_management, :boolean, default: false, null: false)
+ end
+end
diff --git a/db/post_migrate/20240118125559_convert_ci_pipeline_variables_to_list_partitioning_adds_fk_to_ci_pipelines.rb b/db/post_migrate/20240118125559_convert_ci_pipeline_variables_to_list_partitioning_adds_fk_to_ci_pipelines.rb
new file mode 100644
index 00000000000..5d6b4fce30a
--- /dev/null
+++ b/db/post_migrate/20240118125559_convert_ci_pipeline_variables_to_list_partitioning_adds_fk_to_ci_pipelines.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class ConvertCiPipelineVariablesToListPartitioningAddsFkToCiPipelines < Gitlab::Database::Migration[2.2]
+ milestone '16.9'
+ disable_ddl_transaction!
+
+ include Gitlab::Database::PartitioningMigrationHelpers::TableManagementHelpers
+
+ TABLE_NAME = :ci_pipeline_variables
+ PARENT_TABLE_NAME = :p_ci_pipeline_variables
+ FIRST_PARTITION = [100, 101]
+ PARTITION_COLUMN = :partition_id
+
+ def up
+ convert_table_to_first_list_partition(
+ table_name: TABLE_NAME,
+ partitioning_column: PARTITION_COLUMN,
+ parent_table_name: PARENT_TABLE_NAME,
+ initial_partitioning_value: FIRST_PARTITION
+ )
+ end
+
+ def down
+ revert_converting_table_to_first_list_partition(
+ table_name: TABLE_NAME,
+ partitioning_column: PARTITION_COLUMN,
+ parent_table_name: PARENT_TABLE_NAME,
+ initial_partitioning_value: FIRST_PARTITION
+ )
+ end
+end
diff --git a/db/schema_migrations/20240110092610 b/db/schema_migrations/20240110092610
new file mode 100644
index 00000000000..9eef6bb1e48
--- /dev/null
+++ b/db/schema_migrations/20240110092610
@@ -0,0 +1 @@
+f2f81a5e257e8a90b8f4a3847b7d1722e7b274bc4885c50a4a4e07c0172e49b4 \ No newline at end of file
diff --git a/db/schema_migrations/20240117081214 b/db/schema_migrations/20240117081214
new file mode 100644
index 00000000000..882545177ac
--- /dev/null
+++ b/db/schema_migrations/20240117081214
@@ -0,0 +1 @@
+9b4106a42da1d80bb01f4e1e2b0afd112f09fd33ecb93e99f001321edbc5776d \ No newline at end of file
diff --git a/db/schema_migrations/20240118125559 b/db/schema_migrations/20240118125559
new file mode 100644
index 00000000000..79bce6d6c81
--- /dev/null
+++ b/db/schema_migrations/20240118125559
@@ -0,0 +1 @@
+27ef688c8ed556edf45f4b3dcb4a2aba89a2839339a3a5339574c3a33d34f68f \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 182e150704e..a4efb7a734c 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -12633,6 +12633,7 @@ CREATE TABLE application_settings (
lock_toggle_security_policies_policy_scope boolean DEFAULT false NOT NULL,
rate_limits jsonb DEFAULT '{}'::jsonb NOT NULL,
elasticsearch_max_code_indexing_concurrency integer DEFAULT 30 NOT NULL,
+ enable_member_promotion_management boolean DEFAULT false NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)),
CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)),
@@ -14704,7 +14705,7 @@ CREATE SEQUENCE ci_pipeline_schedules_id_seq
ALTER SEQUENCE ci_pipeline_schedules_id_seq OWNED BY ci_pipeline_schedules.id;
-CREATE TABLE ci_pipeline_variables (
+CREATE TABLE p_ci_pipeline_variables (
key character varying NOT NULL,
value text,
encrypted_value text,
@@ -14714,9 +14715,9 @@ CREATE TABLE ci_pipeline_variables (
partition_id bigint NOT NULL,
raw boolean DEFAULT false NOT NULL,
id bigint NOT NULL,
- pipeline_id bigint NOT NULL,
- CONSTRAINT partitioning_constraint CHECK ((partition_id = ANY (ARRAY[(100)::bigint, (101)::bigint])))
-);
+ pipeline_id bigint NOT NULL
+)
+PARTITION BY LIST (partition_id);
CREATE SEQUENCE ci_pipeline_variables_id_seq
START WITH 1
@@ -14725,7 +14726,20 @@ CREATE SEQUENCE ci_pipeline_variables_id_seq
NO MAXVALUE
CACHE 1;
-ALTER SEQUENCE ci_pipeline_variables_id_seq OWNED BY ci_pipeline_variables.id;
+ALTER SEQUENCE ci_pipeline_variables_id_seq OWNED BY p_ci_pipeline_variables.id;
+
+CREATE TABLE ci_pipeline_variables (
+ key character varying NOT NULL,
+ value text,
+ encrypted_value text,
+ encrypted_value_salt character varying,
+ encrypted_value_iv character varying,
+ variable_type smallint DEFAULT 1 NOT NULL,
+ partition_id bigint NOT NULL,
+ raw boolean DEFAULT false NOT NULL,
+ id bigint DEFAULT nextval('ci_pipeline_variables_id_seq'::regclass) NOT NULL,
+ pipeline_id bigint NOT NULL
+);
CREATE TABLE ci_pipelines (
id integer NOT NULL,
@@ -26801,6 +26815,8 @@ ALTER TABLE ONLY p_ci_builds ATTACH PARTITION ci_builds FOR VALUES IN ('100');
ALTER TABLE ONLY p_ci_builds_metadata ATTACH PARTITION ci_builds_metadata FOR VALUES IN ('100');
+ALTER TABLE ONLY p_ci_pipeline_variables ATTACH PARTITION ci_pipeline_variables FOR VALUES IN ('100', '101');
+
ALTER TABLE ONLY abuse_events ALTER COLUMN id SET DEFAULT nextval('abuse_events_id_seq'::regclass);
ALTER TABLE ONLY abuse_report_assignees ALTER COLUMN id SET DEFAULT nextval('abuse_report_assignees_id_seq'::regclass);
@@ -27035,8 +27051,6 @@ ALTER TABLE ONLY ci_pipeline_schedule_variables ALTER COLUMN id SET DEFAULT next
ALTER TABLE ONLY ci_pipeline_schedules ALTER COLUMN id SET DEFAULT nextval('ci_pipeline_schedules_id_seq'::regclass);
-ALTER TABLE ONLY ci_pipeline_variables ALTER COLUMN id SET DEFAULT nextval('ci_pipeline_variables_id_seq'::regclass);
-
ALTER TABLE ONLY ci_pipelines ALTER COLUMN id SET DEFAULT nextval('ci_pipelines_id_seq'::regclass);
ALTER TABLE ONLY ci_platform_metrics ALTER COLUMN id SET DEFAULT nextval('ci_platform_metrics_id_seq'::regclass);
@@ -27541,6 +27555,8 @@ ALTER TABLE ONLY p_ci_builds_metadata ALTER COLUMN id SET DEFAULT nextval('ci_bu
ALTER TABLE ONLY p_ci_job_annotations ALTER COLUMN id SET DEFAULT nextval('p_ci_job_annotations_id_seq'::regclass);
+ALTER TABLE ONLY p_ci_pipeline_variables ALTER COLUMN id SET DEFAULT nextval('ci_pipeline_variables_id_seq'::regclass);
+
ALTER TABLE ONLY packages_build_infos ALTER COLUMN id SET DEFAULT nextval('packages_build_infos_id_seq'::regclass);
ALTER TABLE ONLY packages_composer_cache_files ALTER COLUMN id SET DEFAULT nextval('packages_composer_cache_files_id_seq'::regclass);
@@ -29127,6 +29143,9 @@ ALTER TABLE ONLY ci_pipeline_schedule_variables
ALTER TABLE ONLY ci_pipeline_schedules
ADD CONSTRAINT ci_pipeline_schedules_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY p_ci_pipeline_variables
+ ADD CONSTRAINT p_ci_pipeline_variables_pkey PRIMARY KEY (id, partition_id);
+
ALTER TABLE ONLY ci_pipeline_variables
ADD CONSTRAINT ci_pipeline_variables_pkey PRIMARY KEY (id, partition_id);
@@ -34800,6 +34819,8 @@ CREATE INDEX index_pipeline_metadata_on_name_text_pattern_pipeline_id ON ci_pipe
CREATE INDEX index_pipeline_metadata_on_pipeline_id_name_text_pattern ON ci_pipeline_metadata USING btree (pipeline_id, name text_pattern_ops);
+CREATE UNIQUE INDEX p_ci_pipeline_variables_pipeline_id_key_partition_id_idx ON ONLY p_ci_pipeline_variables USING btree (pipeline_id, key, partition_id);
+
CREATE UNIQUE INDEX index_pipeline_variables_on_pipeline_id_key_partition_id_unique ON ci_pipeline_variables USING btree (pipeline_id, key, partition_id);
CREATE UNIQUE INDEX index_plan_limits_on_plan_id ON plan_limits USING btree (plan_id);
@@ -35940,6 +35961,8 @@ CREATE INDEX index_web_hooks_on_group_id ON web_hooks USING btree (group_id) WHE
CREATE INDEX index_web_hooks_on_integration_id ON web_hooks USING btree (integration_id);
+CREATE INDEX index_web_hooks_on_project_id_and_id ON web_hooks USING btree (project_id, id) WHERE ((type)::text = 'ProjectHook'::text);
+
CREATE INDEX index_web_hooks_on_project_id_recent_failures ON web_hooks USING btree (project_id, recent_failures);
CREATE INDEX index_web_hooks_on_type ON web_hooks USING btree (type);
@@ -37938,6 +37961,8 @@ ALTER INDEX p_ci_builds_metadata_pkey ATTACH PARTITION ci_builds_metadata_pkey;
ALTER INDEX p_ci_builds_pkey ATTACH PARTITION ci_builds_pkey;
+ALTER INDEX p_ci_pipeline_variables_pkey ATTACH PARTITION ci_pipeline_variables_pkey;
+
ALTER INDEX p_ci_builds_metadata_build_id_idx ATTACH PARTITION index_ci_builds_metadata_on_build_id_and_has_exposed_artifacts;
ALTER INDEX p_ci_builds_metadata_build_id_id_idx ATTACH PARTITION index_ci_builds_metadata_on_build_id_and_id_and_interruptible;
@@ -37984,6 +38009,8 @@ ALTER INDEX p_ci_builds_runner_id_idx ATTACH PARTITION index_ci_builds_runner_id
ALTER INDEX p_ci_builds_user_id_name_idx ATTACH PARTITION index_partial_ci_builds_on_user_id_name_parser_features;
+ALTER INDEX p_ci_pipeline_variables_pipeline_id_key_partition_id_idx ATTACH PARTITION index_pipeline_variables_on_pipeline_id_key_partition_id_unique;
+
ALTER INDEX p_ci_builds_user_id_name_created_at_idx ATTACH PARTITION index_secure_ci_builds_on_user_id_name_created_at;
ALTER INDEX p_ci_builds_name_id_idx ATTACH PARTITION index_security_ci_builds_on_name_and_id_parser_features;
@@ -39198,7 +39225,7 @@ ALTER TABLE ONLY timelogs
ALTER TABLE ONLY boards
ADD CONSTRAINT fk_f15266b5f9 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
-ALTER TABLE ONLY ci_pipeline_variables
+ALTER TABLE p_ci_pipeline_variables
ADD CONSTRAINT fk_f29c5f4380 FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
ALTER TABLE ONLY zoekt_indices
diff --git a/doc/development/fe_guide/customizable_dashboards.md b/doc/development/fe_guide/customizable_dashboards.md
index 9ba0d4bb40c..500c29655df 100644
--- a/doc/development/fe_guide/customizable_dashboards.md
+++ b/doc/development/fe_guide/customizable_dashboards.md
@@ -74,7 +74,7 @@ export const pageViewsOverTime = {
dimensions: [],
filters: [
{
- member: 'TrackedEvents.event',
+ member: 'TrackedEvents.eventName',
operator: 'equals',
values: ['page_view']
}
diff --git a/doc/subscriptions/gitlab_dedicated/index.md b/doc/subscriptions/gitlab_dedicated/index.md
index e6a6aace3b4..4e1b36cf8a4 100644
--- a/doc/subscriptions/gitlab_dedicated/index.md
+++ b/doc/subscriptions/gitlab_dedicated/index.md
@@ -110,9 +110,9 @@ With GitLab Dedicated, you must [install the GitLab Runner application](https://
To help you migrate your data to GitLab Dedicated, you can choose from the following options:
-1. When migrating from another GitLab instance, you can either:
- - Use the UI, by using [direct transfer](../../user/group/import/index.md) to import groups and projects.
- - Use APIs, including the [group import API](../../api/group_import_export.md) and [project import API](../../api/project_import_export.md).
+1. When migrating from another GitLab instance, you can import groups and projects by either:
+ - Using [direct transfer](../../user/group/import/index.md).
+ - Using the [direct transfer](../../api/bulk_imports.md) API.
1. When migrating from third-party services, you can use [the GitLab importers](../../user/project/import/index.md#supported-import-sources).
1. You can also engage [Professional Services](../../user/project/import/index.md#migrate-by-engaging-professional-services).
diff --git a/doc/topics/git/git_rebase.md b/doc/topics/git/git_rebase.md
index 63ac1d845e0..d9bf9f758f6 100644
--- a/doc/topics/git/git_rebase.md
+++ b/doc/topics/git/git_rebase.md
@@ -263,3 +263,35 @@ you can't approve a merge request if you have rebased it.
- [Numerous undo possibilities in Git](numerous_undo_possibilities_in_git/index.md#undo-staged-local-changes-without-modifying-history)
- [Git documentation for branches and rebases](https://git-scm.com/book/en/v2/Git-Branching-Rebasing)
- [Project squash and merge settings](../../user/project/merge_requests/squash_and_merge.md#configure-squash-options-for-a-project)
+
+## Troubleshooting
+
+### `Unmergeable state` after `/rebase` quick action
+
+The `/rebase` command schedules a background task. The task attempts to rebase
+the changes in the source branch on the latest commit of the target branch.
+If, after using the `/rebase`
+[quick action](../../user/project/quick_actions.md#issues-merge-requests-and-epics),
+you see this error, a rebase cannot be scheduled:
+
+```plaintext
+This merge request is currently in an unmergeable state, and cannot be rebased.
+```
+
+This error occurs if any of these conditions are true:
+
+- Conflicts exist between the source and target branches.
+- The source branch contains no commits.
+- Either the source or target branch does not exist.
+- An error has occurred, resulting in no diff being generated.
+
+To resolve the `unmergeable state` error:
+
+1. Resolve any merge conflicts.
+1. Confirm the source branch exists, and has commits.
+1. Confirm the target branch exists.
+1. Confirm the diff has been generated.
+
+### `/merge` quick action ignored after `/rebase`
+
+If `/rebase` is used, `/merge` is ignored to avoid a race condition where the source branch is merged or deleted before it is rebased.
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index be119275cd3..bd1eab6a3e1 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -90,7 +90,7 @@ To auto-format this table, use the VS Code Markdown Table formatter: `https://do
| `/ready` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Set the [ready status](merge_requests/drafts.md#mark-merge-requests-as-ready) ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90361) in GitLab 15.1). |
| `/reassign @user1 @user2` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Replace current assignees with those specified. |
| `/reassign_reviewer @user1 @user2` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Replace current reviewers with those specified. |
-| `/rebase` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Rebase source branch. This schedules a background task that attempts to rebase the changes in the source branch on the latest commit of the target branch. If `/rebase` is used, `/merge` is ignored to avoid a race condition where the source branch is merged or deleted before it is rebased. If there are merge conflicts, GitLab displays a message that a rebase cannot be scheduled. Rebase failures are displayed with the merge request status. |
+| `/rebase` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Rebase source branch on the latest commit of the target branch. For help, see [troubleshooting `git rebase`](../../topics/git/git_rebase.md#troubleshooting). |
| `/relabel ~label1 ~label2` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Replace current labels with those specified. |
| `/relate #issue1 #issue2` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Mark issues as related. |
| `/remove_child_epic <epic>` | **{dotted-circle}** No | **{dotted-circle}** No | **{check-circle}** Yes | Remove child epic from `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic. |
diff --git a/lib/gitlab/internal_events.rb b/lib/gitlab/internal_events.rb
index 12609e9be35..4b0ed90c391 100644
--- a/lib/gitlab/internal_events.rb
+++ b/lib/gitlab/internal_events.rb
@@ -6,6 +6,8 @@ module Gitlab
InvalidPropertyError = Class.new(StandardError)
InvalidPropertyTypeError = Class.new(StandardError)
+ SNOWPLOW_EMITTER_BUFFER_SIZE = 100
+
class << self
include Gitlab::Tracking::Helpers
include Gitlab::Utils::StrongMemoize
@@ -120,7 +122,7 @@ module Gitlab
return unless app_id.present? && host.present?
- GitlabSDK::Client.new(app_id: app_id, host: host)
+ GitlabSDK::Client.new(app_id: app_id, host: host, buffer_size: SNOWPLOW_EMITTER_BUFFER_SIZE)
end
strong_memoize_attr :gitlab_sdk_client
end
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 74267874eeb..c391f4a215e 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -100,6 +100,7 @@ RSpec.describe 'Database schema', feature_category: :database do
p_batched_git_ref_updates_deletions: %w[project_id partition_id],
p_catalog_resource_sync_events: %w[catalog_resource_id project_id partition_id],
p_ci_finished_build_ch_sync_events: %w[build_id],
+ p_ci_pipeline_variables: %w[partition_id],
product_analytics_events_experimental: %w[event_id txn_id user_id],
project_build_artifacts_size_refreshes: %w[last_job_artifact_id],
project_data_transfers: %w[project_id namespace_id],
diff --git a/spec/features/registrations/oauth_registration_spec.rb b/spec/features/registrations/oauth_registration_spec.rb
index eb21d285bd0..369df614083 100644
--- a/spec/features/registrations/oauth_registration_spec.rb
+++ b/spec/features/registrations/oauth_registration_spec.rb
@@ -107,7 +107,7 @@ RSpec.describe 'OAuth Registration', :js, :allow_forgery_protection, feature_cat
it 'redirects to the group page with all the projects/groups invitations accepted' do
visit invite_path(group_invite.raw_invite_token, extra_params)
- click_link_or_button "oauth-login-#{provider}"
+ click_link_or_button Gitlab::Auth::OAuth::Provider.label_for(provider)
expect(page).to have_content('You have been granted Owner access to group Owned.')
expect(page).to have_current_path(group_path(group), ignore_query: true)
diff --git a/spec/frontend/organizations/groups_and_projects/components/app_spec.js b/spec/frontend/organizations/groups_and_projects/components/app_spec.js
index 7215750c3c7..a9638369a01 100644
--- a/spec/frontend/organizations/groups_and_projects/components/app_spec.js
+++ b/spec/frontend/organizations/groups_and_projects/components/app_spec.js
@@ -33,6 +33,7 @@ describe('GroupsAndProjectsApp', () => {
const findFilteredSearchBar = () => wrapper.findComponent(FilteredSearchBar);
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findSort = () => wrapper.findComponent(GlSorting);
+ const findProjectsView = () => wrapper.findComponent(ProjectsView);
describe.each`
display | expectedComponent | expectedDisplayListboxSelectedProp
@@ -160,4 +161,70 @@ describe('GroupsAndProjectsApp', () => {
]);
});
});
+
+ describe('when page is changed', () => {
+ const mockEndCursor = 'mockEndCursor';
+ const mockStartCursor = 'mockStartCursor';
+
+ describe('when going to next page', () => {
+ beforeEach(() => {
+ createComponent({ routeQuery: { display: RESOURCE_TYPE_PROJECTS } });
+ findProjectsView().vm.$emit('page-change', {
+ endCursor: mockEndCursor,
+ startCursor: null,
+ hasPreviousPage: true,
+ });
+ });
+
+ it('sets `end_cursor` query string', () => {
+ expect(routerMock.push).toHaveBeenCalledWith({
+ query: { display: RESOURCE_TYPE_PROJECTS, end_cursor: mockEndCursor },
+ });
+ });
+ });
+
+ describe('when going to previous page', () => {
+ it('sets `start_cursor` query string', () => {
+ createComponent({
+ routeQuery: {
+ display: RESOURCE_TYPE_PROJECTS,
+ start_cursor: mockStartCursor,
+ end_cursor: mockEndCursor,
+ },
+ });
+
+ findProjectsView().vm.$emit('page-change', {
+ endCursor: null,
+ startCursor: mockStartCursor,
+ hasPreviousPage: true,
+ });
+
+ expect(routerMock.push).toHaveBeenCalledWith({
+ query: { display: RESOURCE_TYPE_PROJECTS, start_cursor: mockStartCursor },
+ });
+ });
+
+ describe('when going to the first page', () => {
+ it('removes `start_cursor` and `end_cursor` query string', () => {
+ createComponent({
+ routeQuery: {
+ display: RESOURCE_TYPE_PROJECTS,
+ start_cursor: mockStartCursor,
+ end_cursor: mockEndCursor,
+ },
+ });
+
+ findProjectsView().vm.$emit('page-change', {
+ endCursor: null,
+ startCursor: mockStartCursor,
+ hasPreviousPage: false,
+ });
+
+ expect(routerMock.push).toHaveBeenCalledWith({
+ query: { display: RESOURCE_TYPE_PROJECTS },
+ });
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/organizations/shared/components/projects_view_spec.js b/spec/frontend/organizations/shared/components/projects_view_spec.js
index c406ba2cd47..12fe8ed353d 100644
--- a/spec/frontend/organizations/shared/components/projects_view_spec.js
+++ b/spec/frontend/organizations/shared/components/projects_view_spec.js
@@ -1,15 +1,21 @@
import VueApollo from 'vue-apollo';
import Vue from 'vue';
-import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import { GlLoadingIcon, GlEmptyState, GlKeysetPagination } from '@gitlab/ui';
import ProjectsView from '~/organizations/shared/components/projects_view.vue';
import projectsQuery from '~/organizations/shared/graphql/queries/projects.query.graphql';
import { formatProjects } from '~/organizations/shared/utils';
import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
import { createAlert } from '~/alert';
+import { DEFAULT_PER_PAGE } from '~/api';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { organizationProjects as nodes, pageInfo, pageInfoEmpty } from '~/organizations/mock_data';
+import {
+ organizationProjects as nodes,
+ pageInfo,
+ pageInfoEmpty,
+ pageInfoOnePage,
+} from '~/organizations/mock_data';
jest.mock('~/alert');
@@ -56,6 +62,7 @@ describe('ProjectsView', () => {
});
};
+ const findPagination = () => wrapper.findComponent(GlKeysetPagination);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findProjectsList = () => wrapper.findComponent(ProjectsList);
@@ -134,6 +141,139 @@ describe('ProjectsView', () => {
});
});
});
+
+ describe('when there is one page of projects', () => {
+ beforeEach(async () => {
+ createComponent({
+ handler: jest.fn().mockResolvedValue({
+ data: {
+ organization: {
+ id: defaultProvide.organizationGid,
+ projects: {
+ nodes,
+ pageInfo: pageInfoOnePage,
+ },
+ },
+ },
+ }),
+ });
+ await waitForPromises();
+ });
+
+ it('does not render pagination', () => {
+ expect(findPagination().exists()).toBe(false);
+ });
+ });
+
+ describe('when there is a next page of projects', () => {
+ const mockEndCursor = 'mockEndCursor';
+ const handler = jest.fn().mockResolvedValue({
+ data: {
+ organization: {
+ id: defaultProvide.organizationGid,
+ projects: {
+ nodes,
+ pageInfo: {
+ ...pageInfo,
+ hasNextPage: true,
+ hasPreviousPage: false,
+ },
+ },
+ },
+ },
+ });
+
+ beforeEach(async () => {
+ createComponent({ handler });
+ await waitForPromises();
+ });
+
+ it('renders pagination', () => {
+ expect(findPagination().exists()).toBe(true);
+ });
+
+ describe('when next button is clicked', () => {
+ beforeEach(async () => {
+ findPagination().vm.$emit('next', mockEndCursor);
+ await waitForPromises();
+ });
+
+ it('calls query with correct variables', () => {
+ expect(handler).toHaveBeenCalledWith({
+ after: mockEndCursor,
+ before: null,
+ first: DEFAULT_PER_PAGE,
+ id: defaultProvide.organizationGid,
+ last: null,
+ });
+ });
+
+ it('emits `page-change` event', () => {
+ expect(wrapper.emitted('page-change')[1]).toEqual([
+ {
+ endCursor: mockEndCursor,
+ startCursor: null,
+ hasPreviousPage: false,
+ },
+ ]);
+ });
+ });
+ });
+
+ describe('when there is a previous page of projects', () => {
+ const mockStartCursor = 'mockStartCursor';
+ const handler = jest.fn().mockResolvedValue({
+ data: {
+ organization: {
+ id: defaultProvide.organizationGid,
+ projects: {
+ nodes,
+ pageInfo: {
+ ...pageInfo,
+ hasNextPage: false,
+ hasPreviousPage: true,
+ },
+ },
+ },
+ },
+ });
+
+ beforeEach(async () => {
+ createComponent({ handler });
+ await waitForPromises();
+ });
+
+ it('renders pagination', () => {
+ expect(findPagination().exists()).toBe(true);
+ });
+
+ describe('when next button is clicked', () => {
+ beforeEach(async () => {
+ findPagination().vm.$emit('prev', mockStartCursor);
+ await waitForPromises();
+ });
+
+ it('calls query with correct variables', () => {
+ expect(handler).toHaveBeenCalledWith({
+ after: null,
+ before: mockStartCursor,
+ first: null,
+ id: defaultProvide.organizationGid,
+ last: DEFAULT_PER_PAGE,
+ });
+ });
+
+ it('emits `page-change` event', () => {
+ expect(wrapper.emitted('page-change')[1]).toEqual([
+ {
+ endCursor: null,
+ startCursor: mockStartCursor,
+ hasPreviousPage: true,
+ },
+ ]);
+ });
+ });
+ });
});
describe('when API call is not successful', () => {
diff --git a/spec/frontend/organizations/shared/utils_spec.js b/spec/frontend/organizations/shared/utils_spec.js
index d8d5279b670..29d7090c576 100644
--- a/spec/frontend/organizations/shared/utils_spec.js
+++ b/spec/frontend/organizations/shared/utils_spec.js
@@ -1,4 +1,4 @@
-import { formatProjects, formatGroups } from '~/organizations/shared/utils';
+import { formatProjects, formatGroups, onPageChange } from '~/organizations/shared/utils';
import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { organizationProjects, organizationGroups } from '~/organizations/mock_data';
@@ -35,3 +35,42 @@ describe('formatGroups', () => {
expect(formattedGroups.length).toBe(organizationGroups.nodes.length);
});
});
+
+describe('onPageChange', () => {
+ const mockRouteQuery = { start_cursor: 'mockStartCursor', end_cursor: 'mockEndCursor' };
+
+ describe('when `startCursor` is defined and `hasPreviousPage` is `true`', () => {
+ it('sets start cursor query param', () => {
+ expect(
+ onPageChange({
+ startCursor: 'newMockStartCursor',
+ hasPreviousPage: true,
+ routeQuery: mockRouteQuery,
+ }),
+ ).toEqual({ start_cursor: 'newMockStartCursor' });
+ });
+ });
+
+ describe('when `startCursor` is defined and `hasPreviousPage` is `false`', () => {
+ it('does not set any query params', () => {
+ expect(
+ onPageChange({
+ startCursor: 'newMockStartCursor',
+ hasPreviousPage: false,
+ routeQuery: mockRouteQuery,
+ }),
+ ).toEqual({});
+ });
+ });
+
+ describe('when `endCursor` is defined', () => {
+ it('sets end cursor query param', () => {
+ expect(
+ onPageChange({
+ endCursor: 'newMockEndCursor',
+ routeQuery: mockRouteQuery,
+ }),
+ ).toEqual({ end_cursor: 'newMockEndCursor' });
+ });
+ });
+});
diff --git a/spec/lib/gitlab/database/sharding_key_spec.rb b/spec/lib/gitlab/database/sharding_key_spec.rb
index dfd78bfacba..fb9dd3468a3 100644
--- a/spec/lib/gitlab/database/sharding_key_spec.rb
+++ b/spec/lib/gitlab/database/sharding_key_spec.rb
@@ -8,7 +8,8 @@ RSpec.describe 'new tables missing sharding_key', feature_category: :cell do
let(:allowed_to_be_missing_sharding_key) do
[
'abuse_report_assignees', # https://gitlab.com/gitlab-org/gitlab/-/issues/432365
- 'sbom_occurrences_vulnerabilities' # https://gitlab.com/gitlab-org/gitlab/-/issues/432900
+ 'sbom_occurrences_vulnerabilities', # https://gitlab.com/gitlab-org/gitlab/-/issues/432900
+ 'p_ci_pipeline_variables' # https://gitlab.com/gitlab-org/gitlab/-/issues/436360
]
end
diff --git a/spec/lib/gitlab/internal_events_spec.rb b/spec/lib/gitlab/internal_events_spec.rb
index 4e475cf9a1d..a1bd265d349 100644
--- a/spec/lib/gitlab/internal_events_spec.rb
+++ b/spec/lib/gitlab/internal_events_spec.rb
@@ -309,7 +309,7 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana
allow(GitlabSDK::Client)
.to receive(:new)
- .with(app_id: app_id, host: url)
+ .with(app_id: app_id, host: url, buffer_size: described_class::SNOWPLOW_EMITTER_BUFFER_SIZE)
.and_return(sdk_client)
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index b4003469ebb..18060d29ad5 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -124,6 +124,8 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
it { is_expected.to validate_inclusion_of(:allow_project_creation_for_guest_and_below).in_array([true, false]) }
+ it { is_expected.to validate_inclusion_of(:enable_member_promotion_management).in_array([true, false]) }
+
it { is_expected.to validate_inclusion_of(:deny_all_requests_except_allowed).in_array([true, false]) }
it 'ensures max_pages_size is an integer greater than 0 (or equal to 0 to indicate unlimited/maximum)' do
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 83849df73dc..cc45cb1292d 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -132,7 +132,7 @@ module LoginHelpers
visit new_user_registration_path
expect(page).to have_content('Create an account using').or(have_content('Register with'))
- click_link_or_button "oauth-login-#{provider}"
+ click_link_or_button Gitlab::Auth::OAuth::Provider.label_for(provider)
end
def fake_successful_webauthn_authentication