diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-22 15:08:07 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-22 15:08:07 +0300 |
commit | e5c31c104e19a08546b17b34b7f1563cce3f89e6 (patch) | |
tree | 4f05401dc288370583328a94c1bbf1f1de9ace64 | |
parent | 56865fdf95db03cc0ccd01a88d9457ba0a050153 (diff) |
Add latest changes from gitlab-org/gitlab@master
38 files changed, 590 insertions, 52 deletions
@@ -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 |