diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-06 03:14:55 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-06 03:14:55 +0300 |
commit | d39c778244590f478537df87ed01dde2705350a8 (patch) | |
tree | 5792def7c7c01effeeea50eb7fba02d0a53b0169 | |
parent | b7d0ee2a31d4d8b8037c07cb1df7c123d2e754b5 (diff) |
Add latest changes from gitlab-org/gitlab@master
51 files changed, 577 insertions, 297 deletions
diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml index d59f5f4c47d..dfcc1e48ddd 100644 --- a/.rubocop_todo/rspec/missing_feature_category.yml +++ b/.rubocop_todo/rspec/missing_feature_category.yml @@ -3663,7 +3663,6 @@ RSpec/MissingFeatureCategory: - 'spec/lib/gitlab/database/migrations/test_background_runner_spec.rb' - 'spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb' - 'spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb' - - 'spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb' - 'spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb' - 'spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb' - 'spec/lib/gitlab/database/partitioning/partition_manager_spec.rb' @@ -3674,7 +3673,6 @@ RSpec/MissingFeatureCategory: - 'spec/lib/gitlab/database/partitioning/time_partition_spec.rb' - 'spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb' - 'spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb' - - 'spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb' - 'spec/lib/gitlab/database/partitioning_spec.rb' - 'spec/lib/gitlab/database/pg_class_spec.rb' - 'spec/lib/gitlab/database/postgres_constraint_spec.rb' diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue index 750ec0aa0c6..e03c587567e 100644 --- a/app/assets/javascripts/super_sidebar/components/user_bar.vue +++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue @@ -42,13 +42,14 @@ export default { false, ), todoList: __('To-Do list'), + stopImpersonating: __('Stop impersonating'), }, directives: { GlTooltip: GlTooltipDirective, GlModal: GlModalDirective, SafeHtml, }, - inject: ['rootPath'], + inject: ['rootPath', 'isImpersonating'], props: { sidebarData: { type: Object, @@ -114,6 +115,19 @@ export default { <search-modal /> <user-menu :data="sidebarData" /> + + <gl-button + v-if="isImpersonating" + v-gl-tooltip + :href="sidebarData.stop_impersonation_path" + :title="$options.i18n.stopImpersonating" + :aria-label="$options.i18n.stopImpersonating" + icon="incognito" + variant="confirm" + category="tertiary" + data-method="delete" + data-testid="stop-impersonation-btn" + /> </div> <div class="gl-display-flex gl-justify-content-space-between gl-px-3 gl-py-2 gl-gap-2"> <counter diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js index c5e8c68b940..58b49f218ad 100644 --- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js +++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; -import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils'; import { initStatusTriggers } from '../header'; import createStore from './components/global_search/store'; import { @@ -29,6 +29,7 @@ export const initSuperSidebar = () => { const searchData = convertObjectPropsToCamelCase(sidebarData.search); const { searchPath, issuesPath, mrPath, autocompletePath, searchContext } = searchData; + const isImpersonating = parseBoolean(sidebarData.is_impersonating); return new Vue({ el, @@ -37,6 +38,7 @@ export const initSuperSidebar = () => { provide: { rootPath, toggleNewNavEndpoint, + isImpersonating, }, store: createStore({ searchPath, diff --git a/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue index 45c50dce8ce..9b45e969c90 100644 --- a/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue +++ b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue @@ -13,6 +13,11 @@ export default { GlCollapsibleListbox, }, props: { + block: { + type: Boolean, + required: false, + default: false, + }, label: { type: String, required: true, @@ -176,6 +181,7 @@ export default { <gl-collapsible-listbox ref="listbox" v-model="selected" + :block="block" :header-text="headerText" :reset-button-label="resetButtonLabel" :toggle-text="toggleText" diff --git a/app/assets/javascripts/vue_shared/components/entity_select/init_project_selects.js b/app/assets/javascripts/vue_shared/components/entity_select/init_project_selects.js index 1afbeda74c4..12db70d8e9c 100644 --- a/app/assets/javascripts/vue_shared/components/entity_select/init_project_selects.js +++ b/app/assets/javascripts/vue_shared/components/entity_select/init_project_selects.js @@ -20,6 +20,8 @@ export const initProjectSelects = () => { orderBy, selected: initialSelection, } = el.dataset; + const block = parseBoolean(el.dataset.block); + const withShared = parseBoolean(el.dataset.withShared); const includeSubgroups = parseBoolean(el.dataset.includeSubgroups); const membership = parseBoolean(el.dataset.membership); const hasHtmlLabel = parseBoolean(el.dataset.hasHtmlLabel); @@ -37,6 +39,8 @@ export const initProjectSelects = () => { groupId, userId, orderBy, + block, + withShared, includeSubgroups, membership, initialSelection, diff --git a/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue index 393991d746e..7af3819f2a5 100644 --- a/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue +++ b/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue @@ -20,6 +20,11 @@ export default { SafeHtml, }, props: { + block: { + type: Boolean, + required: false, + default: false, + }, label: { type: String, required: true, @@ -47,6 +52,11 @@ export default { required: false, default: null, }, + withShared: { + type: Boolean, + required: false, + default: true, + }, includeSubgroups: { type: Boolean, required: false, @@ -86,7 +96,7 @@ export default { if (this.groupId) { return Api.groupProjects(this.groupId, searchString, { ...commonParams, - with_shared: true, + with_shared: this.withShared, include_subgroups: this.includeSubgroups, simple: true, }); @@ -99,7 +109,7 @@ export default { this.userId, searchString, { - with_shared: true, + with_shared: this.withShared, include_subgroups: this.includeSubgroups, }, (res) => ({ data: res }), @@ -154,6 +164,7 @@ export default { :default-toggle-text="$options.i18n.searchForProject" :fetch-items="fetchProjects" :fetch-initial-selection-text="fetchProjectName" + :block="block" clearable > <template v-if="hasHtmlLabel" #label> diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb index 6a19943afa6..341937e78de 100644 --- a/app/helpers/sidebars_helper.rb +++ b/app/helpers/sidebars_helper.rb @@ -87,7 +87,9 @@ module SidebarsHelper search: search_data, pinned_items: user.pinned_nav_items[panel_type] || [], panel_type: panel_type, - update_pins_url: pins_url + update_pins_url: pins_url, + is_impersonating: impersonating?, + stop_impersonation_path: admin_impersonation_path } end @@ -323,6 +325,10 @@ module SidebarsHelper count.to_s end end + + def impersonating? + !!session[:impersonator_id] + end end SidebarsHelper.prepend_mod_with('SidebarsHelper') diff --git a/app/models/work_items/resource_link_event.rb b/app/models/work_items/resource_link_event.rb new file mode 100644 index 00000000000..64d51b2743c --- /dev/null +++ b/app/models/work_items/resource_link_event.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module WorkItems + class ResourceLinkEvent < ResourceEvent + belongs_to :child_work_item, class_name: 'WorkItem' + + validates :child_work_item, presence: true + + enum action: { + add: 1, + remove: 2 + } + end +end diff --git a/app/views/clusters/clusters/_advanced_settings.html.haml b/app/views/clusters/clusters/_advanced_settings.html.haml index b49f1aa061a..a818f8a5c26 100644 --- a/app/views/clusters/clusters/_advanced_settings.html.haml +++ b/app/views/clusters/clusters/_advanced_settings.html.haml @@ -24,6 +24,7 @@ order_by: 'last_activity_at', group_id: group_id, user_id: user_id, + with_shared: true.to_s, include_subgroups: true.to_s, membership: true.to_s, selected: @cluster.management_project_id } } diff --git a/db/docs/resource_link_events.yml b/db/docs/resource_link_events.yml new file mode 100644 index 00000000000..cfa04aa522c --- /dev/null +++ b/db/docs/resource_link_events.yml @@ -0,0 +1,10 @@ +--- +table_name: resource_link_events +classes: +- WorkItems::ResourceLinkEvent +feature_categories: +- planning_analytics +description: Records the change of parent link on work items along with timestamps +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114394 +milestone: '15.11' +gitlab_schema: gitlab_main diff --git a/db/migrate/20230313031351_create_resource_link_events.rb b/db/migrate/20230313031351_create_resource_link_events.rb new file mode 100644 index 00000000000..03f00c9416b --- /dev/null +++ b/db/migrate/20230313031351_create_resource_link_events.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class CreateResourceLinkEvents < Gitlab::Database::Migration[2.1] + def change + create_table :resource_link_events do |t| + t.integer :action, limit: 2, null: false + t.bigint :user_id, null: false + t.references :issue, index: true, null: false, foreign_key: { on_delete: :cascade } + t.references :child_work_item, index: true, null: false, foreign_key: { to_table: :issues, on_delete: :cascade } + t.datetime_with_timezone :created_at, null: false + + t.index :user_id + end + end +end diff --git a/db/migrate/20230329085754_add_foreign_key_to_resource_link_events_on_user.rb b/db/migrate/20230329085754_add_foreign_key_to_resource_link_events_on_user.rb new file mode 100644 index 00000000000..6a167f232ae --- /dev/null +++ b/db/migrate/20230329085754_add_foreign_key_to_resource_link_events_on_user.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddForeignKeyToResourceLinkEventsOnUser < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :resource_link_events, :users, column: :user_id, on_delete: :nullify, validate: true + end + + def down + with_lock_retries do + remove_foreign_key_if_exists :resource_link_events, column: :user_id + end + end +end diff --git a/db/migrate/20230403164454_add_fork_storage_size_columns_to_root_storage_statistics.rb b/db/migrate/20230403164454_add_fork_storage_size_columns_to_root_storage_statistics.rb new file mode 100644 index 00000000000..3428fba669f --- /dev/null +++ b/db/migrate/20230403164454_add_fork_storage_size_columns_to_root_storage_statistics.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddForkStorageSizeColumnsToRootStorageStatistics < Gitlab::Database::Migration[2.1] + enable_lock_retries! + + def change + add_column :namespace_root_storage_statistics, :public_forks_storage_size, :bigint, default: 0, null: false + add_column :namespace_root_storage_statistics, :internal_forks_storage_size, :bigint, default: 0, null: false + add_column :namespace_root_storage_statistics, :private_forks_storage_size, :bigint, default: 0, null: false + end +end diff --git a/db/schema_migrations/20230313031351 b/db/schema_migrations/20230313031351 new file mode 100644 index 00000000000..37a57006cae --- /dev/null +++ b/db/schema_migrations/20230313031351 @@ -0,0 +1 @@ +44dc97ac36a6edcd0c0dba76f6b60204b72c005da7bd793af4ac7832d949bd0b
\ No newline at end of file diff --git a/db/schema_migrations/20230329085754 b/db/schema_migrations/20230329085754 new file mode 100644 index 00000000000..fd2687b225f --- /dev/null +++ b/db/schema_migrations/20230329085754 @@ -0,0 +1 @@ +52c5c662dc46313dece9ed9228af5ea2734f0fc4872ba0f6a762e77437b9564e
\ No newline at end of file diff --git a/db/schema_migrations/20230403164454 b/db/schema_migrations/20230403164454 new file mode 100644 index 00000000000..0283a3b1612 --- /dev/null +++ b/db/schema_migrations/20230403164454 @@ -0,0 +1 @@ +ff04f9ef9bb479b85223e361b96c921e25b436a86a0041627b595c3635848a5b
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 0266b37c320..06e4541147b 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -18623,7 +18623,10 @@ CREATE TABLE namespace_root_storage_statistics ( dependency_proxy_size bigint DEFAULT 0 NOT NULL, notification_level smallint DEFAULT 100 NOT NULL, container_registry_size bigint DEFAULT 0 NOT NULL, - registry_size_estimated boolean DEFAULT false NOT NULL + registry_size_estimated boolean DEFAULT false NOT NULL, + public_forks_storage_size bigint DEFAULT 0 NOT NULL, + internal_forks_storage_size bigint DEFAULT 0 NOT NULL, + private_forks_storage_size bigint DEFAULT 0 NOT NULL ); CREATE TABLE namespace_settings ( @@ -21740,6 +21743,24 @@ CREATE SEQUENCE resource_label_events_id_seq ALTER SEQUENCE resource_label_events_id_seq OWNED BY resource_label_events.id; +CREATE TABLE resource_link_events ( + id bigint NOT NULL, + action smallint NOT NULL, + user_id bigint NOT NULL, + issue_id bigint NOT NULL, + child_work_item_id bigint NOT NULL, + created_at timestamp with time zone NOT NULL +); + +CREATE SEQUENCE resource_link_events_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE resource_link_events_id_seq OWNED BY resource_link_events.id; + CREATE TABLE resource_milestone_events ( id bigint NOT NULL, user_id bigint, @@ -25341,6 +25362,8 @@ ALTER TABLE ONLY resource_iteration_events ALTER COLUMN id SET DEFAULT nextval(' ALTER TABLE ONLY resource_label_events ALTER COLUMN id SET DEFAULT nextval('resource_label_events_id_seq'::regclass); +ALTER TABLE ONLY resource_link_events ALTER COLUMN id SET DEFAULT nextval('resource_link_events_id_seq'::regclass); + ALTER TABLE ONLY resource_milestone_events ALTER COLUMN id SET DEFAULT nextval('resource_milestone_events_id_seq'::regclass); ALTER TABLE ONLY resource_state_events ALTER COLUMN id SET DEFAULT nextval('resource_state_events_id_seq'::regclass); @@ -27703,6 +27726,9 @@ ALTER TABLE ONLY resource_iteration_events ALTER TABLE ONLY resource_label_events ADD CONSTRAINT resource_label_events_pkey PRIMARY KEY (id); +ALTER TABLE ONLY resource_link_events + ADD CONSTRAINT resource_link_events_pkey PRIMARY KEY (id); + ALTER TABLE ONLY resource_milestone_events ADD CONSTRAINT resource_milestone_events_pkey PRIMARY KEY (id); @@ -31928,6 +31954,12 @@ CREATE INDEX index_resource_label_events_on_merge_request_id_label_id_action ON CREATE INDEX index_resource_label_events_on_user_id ON resource_label_events USING btree (user_id); +CREATE INDEX index_resource_link_events_on_child_work_item_id ON resource_link_events USING btree (child_work_item_id); + +CREATE INDEX index_resource_link_events_on_issue_id ON resource_link_events USING btree (issue_id); + +CREATE INDEX index_resource_link_events_on_user_id ON resource_link_events USING btree (user_id); + CREATE INDEX index_resource_milestone_events_created_at ON resource_milestone_events USING btree (created_at); CREATE INDEX index_resource_milestone_events_on_issue_id ON resource_milestone_events USING btree (issue_id); @@ -34950,6 +34982,9 @@ ALTER TABLE ONLY namespace_bans ALTER TABLE ONLY gitlab_subscriptions ADD CONSTRAINT fk_bd0c4019c3 FOREIGN KEY (hosted_plan_id) REFERENCES plans(id) ON DELETE CASCADE; +ALTER TABLE ONLY resource_link_events + ADD CONSTRAINT fk_bd4ae15ce4 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL; + ALTER TABLE ONLY metrics_users_starred_dashboards ADD CONSTRAINT fk_bd6ae32fac FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; @@ -35310,6 +35345,9 @@ ALTER TABLE ONLY audit_events_external_audit_event_destinations ALTER TABLE ONLY operations_user_lists ADD CONSTRAINT fk_rails_0c716e079b FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY resource_link_events + ADD CONSTRAINT fk_rails_0cea73eba5 FOREIGN KEY (child_work_item_id) REFERENCES issues(id) ON DELETE CASCADE; + ALTER TABLE ONLY geo_node_statuses ADD CONSTRAINT fk_rails_0ecc699c2a FOREIGN KEY (geo_node_id) REFERENCES geo_nodes(id) ON DELETE CASCADE; @@ -36696,6 +36734,9 @@ ALTER TABLE ONLY merge_request_reviewers ALTER TABLE ONLY ci_running_builds ADD CONSTRAINT fk_rails_da45cfa165_p FOREIGN KEY (partition_id, build_id) REFERENCES ci_builds(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE; +ALTER TABLE ONLY resource_link_events + ADD CONSTRAINT fk_rails_da5dd8a56f FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE; + ALTER TABLE ONLY jira_imports ADD CONSTRAINT fk_rails_da617096ce FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL; diff --git a/lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb b/lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb index f6cec311225..afca2368126 100644 --- a/lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb +++ b/lib/gitlab/database/partitioning/convert_table_to_first_list_partition.rb @@ -8,6 +8,8 @@ module Gitlab SQL_STATEMENT_SEPARATOR = ";\n\n" + PARTITIONING_CONSTRAINT_NAME = 'partitioning_constraint' + attr_reader :partitioning_column, :table_name, :parent_table_name, :zero_partition_value def initialize( @@ -23,10 +25,10 @@ module Gitlab @lock_tables = Array.wrap(lock_tables) end - def prepare_for_partitioning + def prepare_for_partitioning(async: false) assert_existing_constraints_partitionable - add_partitioning_check_constraint + add_partitioning_check_constraint(async: async) end def revert_preparation_for_partitioning @@ -121,16 +123,17 @@ module Gitlab constraints_on_column = Gitlab::Database::PostgresConstraint .by_table_identifier(table_identifier) .check_constraints - .valid .including_column(partitioning_column) - constraints_on_column.to_a.find do |constraint| - constraint.definition == "CHECK ((#{partitioning_column} = #{zero_partition_value}))" + check_body = "CHECK ((#{partitioning_column} = #{zero_partition_value}))" + + constraints_on_column.find do |constraint| + constraint.definition.start_with?(check_body) end end def assert_partitioning_constraint_present - return if partitioning_constraint + return if partitioning_constraint&.constraint_valid? raise UnableToPartition, <<~MSG Table #{table_name} is not ready for partitioning. @@ -138,14 +141,43 @@ module Gitlab MSG end - def add_partitioning_check_constraint - return if partitioning_constraint.present? + def add_partitioning_check_constraint(async: false) + return validate_partitioning_constraint_synchronously if partitioning_constraint.present? check_body = "#{partitioning_column} = #{connection.quote(zero_partition_value)}" # Any constraint name would work. The constraint is found based on its definition before partitioning - migration_context.add_check_constraint(table_name, check_body, 'partitioning_constraint') + migration_context.add_check_constraint( + table_name, check_body, PARTITIONING_CONSTRAINT_NAME, + validate: !async + ) + + if async + migration_context.prepare_async_check_constraint_validation( + table_name, name: PARTITIONING_CONSTRAINT_NAME + ) + end + + return if partitioning_constraint.present? + + raise UnableToPartition, <<~MSG + Error adding partitioning constraint `#{PARTITIONING_CONSTRAINT_NAME}` for `#{table_name}` + MSG + end + + def validate_partitioning_constraint_synchronously + if partitioning_constraint.constraint_valid? + return Gitlab::AppLogger.info <<~MSG + Nothing to do, the partitioning constraint exists and is valid for `#{table_name}` + MSG + end + + # Async validations are executed only on .com, we need to validate synchronously for self-managed + migration_context.validate_check_constraint(table_name, partitioning_constraint.name) + return if partitioning_constraint.constraint_valid? - raise UnableToPartition, 'Error adding partitioning constraint' unless partitioning_constraint.present? + raise UnableToPartition, <<~MSG + Error validating partitioning constraint `#{partitioning_constraint.name}` for `#{table_name}` + MSG end def create_parent_table diff --git a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb index 3477f4846b6..5a942577006 100644 --- a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb +++ b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb @@ -252,7 +252,7 @@ module Gitlab create_sync_trigger(source_table_name, trigger_name, function_name) end - def prepare_constraint_for_list_partitioning(table_name:, partitioning_column:, parent_table_name:, initial_partitioning_value:) + def prepare_constraint_for_list_partitioning(table_name:, partitioning_column:, parent_table_name:, initial_partitioning_value:, async: false) validate_not_in_transaction!(:prepare_constraint_for_list_partitioning) Gitlab::Database::Partitioning::ConvertTableToFirstListPartition @@ -261,7 +261,7 @@ module Gitlab parent_table_name: parent_table_name, partitioning_column: partitioning_column, zero_partition_value: initial_partitioning_value - ).prepare_for_partitioning + ).prepare_for_partitioning(async: async) end def revert_preparing_constraint_for_list_partitioning(table_name:, partitioning_column:, parent_table_name:, initial_partitioning_value:) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 114fc340132..56e0c0cd5f2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -42294,6 +42294,9 @@ msgstr "" msgid "Stop Terminal" msgstr "" +msgid "Stop impersonating" +msgstr "" + msgid "Stop impersonation" msgstr "" diff --git a/package.json b/package.json index 673c49fb344..37765ae6204 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@gitlab/visual-review-tools": "1.7.3", "@gitlab/web-ide": "0.0.1-dev-20230323132525", "@mattiasbuelens/web-streams-adapter": "^0.1.0", + "@popperjs/core": "^2.11.2", "@rails/actioncable": "6.1.4-7", "@rails/ujs": "6.1.4-7", "@sourcegraph/code-host-integration": "0.0.84", diff --git a/qa/qa/page/group/menu.rb b/qa/qa/page/group/menu.rb index c0af4af464b..46228926311 100644 --- a/qa/qa/page/group/menu.rb +++ b/qa/qa/page/group/menu.rb @@ -5,7 +5,13 @@ module QA module Group class Menu < Page::Base include SubMenus::Common - include SubMenus::SuperSidebar::Settings if Runtime::Env.super_sidebar_enabled? + + if Runtime::Env.super_sidebar_enabled? + prepend Page::SubMenus::SuperSidebar::Manage + prepend Page::SubMenus::SuperSidebar::Plan + prepend SubMenus::SuperSidebar::Settings + prepend SubMenus::SuperSidebar::Build + end def click_group_members_item hover_group_information do @@ -16,6 +22,8 @@ module QA end def click_subgroup_members_item + return go_to_members if Runtime::Env.super_sidebar_enabled? + hover_subgroup_information do within_submenu do click_element(:sidebar_menu_item_link, menu_item: 'Members') diff --git a/qa/qa/page/group/sub_menus/super_sidebar/build.rb b/qa/qa/page/group/sub_menus/super_sidebar/build.rb new file mode 100644 index 00000000000..704548c1dd0 --- /dev/null +++ b/qa/qa/page/group/sub_menus/super_sidebar/build.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module QA + module Page + module Group + module SubMenus + module SuperSidebar + module Build + extend QA::Page::PageConcern + + def go_to_runners + open_build_submenu("Runners") + end + + private + + def open_build_submenu(sub_menu) + open_submenu("Build", "#build", sub_menu) + end + end + end + end + end + end +end diff --git a/qa/qa/page/group/sub_menus/super_sidebar/common.rb b/qa/qa/page/group/sub_menus/super_sidebar/common.rb deleted file mode 100644 index dc1975c0044..00000000000 --- a/qa/qa/page/group/sub_menus/super_sidebar/common.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module QA - module Page - module Group - module SubMenus - module SuperSidebar - module Common - private - - def open_submenu(parent_menu_name, parent_section_id, sub_menu) - click_element(:sidebar_menu_link, menu_item: parent_menu_name) - - # TODO: it's not possible to add qa-selectors to sub-menu containers at the moment - within(parent_section_id) do - click_element(:sidebar_menu_link, menu_item: sub_menu) - end - end - end - end - end - end - end -end diff --git a/qa/qa/page/group/sub_menus/super_sidebar/settings.rb b/qa/qa/page/group/sub_menus/super_sidebar/settings.rb index b0501454413..4478b4b93b3 100644 --- a/qa/qa/page/group/sub_menus/super_sidebar/settings.rb +++ b/qa/qa/page/group/sub_menus/super_sidebar/settings.rb @@ -8,14 +8,6 @@ module QA module Settings extend QA::Page::PageConcern - def self.included(base) - super - - base.class_eval do - include Common - end - end - def go_to_general_settings open_settings_submenu("General") end diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb index 0b2ea479f17..08bd20d5468 100644 --- a/qa/qa/page/project/menu.rb +++ b/qa/qa/page/project/menu.rb @@ -16,16 +16,14 @@ module QA include SubMenus::Packages if Runtime::Env.super_sidebar_enabled? + include Page::SubMenus::SuperSidebar::Manage include SubMenus::SuperSidebar::Project - include SubMenus::SuperSidebar::Manage include SubMenus::SuperSidebar::Plan include SubMenus::SuperSidebar::Settings include SubMenus::SuperSidebar::Code include SubMenus::SuperSidebar::Build - include SubMenus::SuperSidebar::Secure include SubMenus::SuperSidebar::Operate include SubMenus::SuperSidebar::Monitor - include SubMenus::SuperSidebar::Analyze end def click_merge_requests diff --git a/qa/qa/page/project/sub_menus/issues.rb b/qa/qa/page/project/sub_menus/issues.rb index 7fa19063653..48840c29635 100644 --- a/qa/qa/page/project/sub_menus/issues.rb +++ b/qa/qa/page/project/sub_menus/issues.rb @@ -27,7 +27,7 @@ module QA end end - def go_to_boards + def go_to_issue_boards hover_issues do within_submenu do click_element(:sidebar_menu_item_link, menu_item: 'Boards') diff --git a/qa/qa/page/project/sub_menus/super_sidebar/analyze.rb b/qa/qa/page/project/sub_menus/super_sidebar/analyze.rb deleted file mode 100644 index 0a0735ee3eb..00000000000 --- a/qa/qa/page/project/sub_menus/super_sidebar/analyze.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -module QA - module Page - module Project - module SubMenus - module SuperSidebar - module Analyze - extend QA::Page::PageConcern - - def self.included(base) - super - - base.class_eval do - include QA::Page::Project::SubMenus::SuperSidebar::Common - end - end - - def go_to_value_stream_analytics - open_analyze_submenu('Value stream analytics') - end - - def go_to_contributor_statistics - open_analyze_submenu('Contributor statistics') - end - - def go_to_ci_cd_analytics - open_analyze_submenu('CI/CD analytics') - end - - def go_to_repository_analytics - open_analyze_submenu('Repository analytics') - end - - private - - def open_analyze_submenu(sub_menu) - open_submenu('Analyze', '#analyze', sub_menu) - end - end - end - end - end - end -end diff --git a/qa/qa/page/project/sub_menus/super_sidebar/build.rb b/qa/qa/page/project/sub_menus/super_sidebar/build.rb index 117aade7b1d..f5052051d6a 100644 --- a/qa/qa/page/project/sub_menus/super_sidebar/build.rb +++ b/qa/qa/page/project/sub_menus/super_sidebar/build.rb @@ -8,14 +8,6 @@ module QA module Build extend QA::Page::PageConcern - def self.included(base) - super - - base.class_eval do - include QA::Page::Project::SubMenus::SuperSidebar::Common - end - end - def go_to_pipelines open_build_submenu('Pipelines') end diff --git a/qa/qa/page/project/sub_menus/super_sidebar/code.rb b/qa/qa/page/project/sub_menus/super_sidebar/code.rb index 77b87da545e..8263ada5c23 100644 --- a/qa/qa/page/project/sub_menus/super_sidebar/code.rb +++ b/qa/qa/page/project/sub_menus/super_sidebar/code.rb @@ -8,14 +8,6 @@ module QA module Code extend QA::Page::PageConcern - def self.included(base) - super - - base.class_eval do - include QA::Page::Project::SubMenus::SuperSidebar::Common - end - end - def go_to_repository open_code_submenu('Repository') end diff --git a/qa/qa/page/project/sub_menus/super_sidebar/common.rb b/qa/qa/page/project/sub_menus/super_sidebar/common.rb deleted file mode 100644 index 7cb14f4189e..00000000000 --- a/qa/qa/page/project/sub_menus/super_sidebar/common.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module QA - module Page - module Project - module SubMenus - module SuperSidebar - module Common - private - - def open_submenu(parent_menu_name, parent_section_id, sub_menu) - click_element(:sidebar_menu_link, menu_item: parent_menu_name) - - # TODO: it's not possible to add qa-selectors to sub-menu container - within(parent_section_id) do - click_element(:sidebar_menu_link, menu_item: sub_menu) - end - end - end - end - end - end - end -end diff --git a/qa/qa/page/project/sub_menus/super_sidebar/manage.rb b/qa/qa/page/project/sub_menus/super_sidebar/manage.rb deleted file mode 100644 index 40bbd3b6618..00000000000 --- a/qa/qa/page/project/sub_menus/super_sidebar/manage.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -module QA - module Page - module Project - module SubMenus - module SuperSidebar - module Manage - extend QA::Page::PageConcern - - def self.included(base) - super - - base.class_eval do - include QA::Page::Project::SubMenus::SuperSidebar::Common - end - end - - def go_to_activity - open_manage_submenu('Activity') - end - - def go_to_members - open_manage_submenu('Members') - end - - def go_to_labels - open_manage_submenu('Labels') - end - - def go_to_milestones - open_manage_submenu('Milestones') - end - - private - - def open_manage_submenu(sub_menu) - open_submenu('Manage', '#manage', sub_menu) - end - end - end - end - end - end -end diff --git a/qa/qa/page/project/sub_menus/super_sidebar/monitor.rb b/qa/qa/page/project/sub_menus/super_sidebar/monitor.rb index 2a578ba2e80..391cde6887b 100644 --- a/qa/qa/page/project/sub_menus/super_sidebar/monitor.rb +++ b/qa/qa/page/project/sub_menus/super_sidebar/monitor.rb @@ -8,14 +8,6 @@ module QA module Monitor extend QA::Page::PageConcern - def self.included(base) - super - - base.class_eval do - include QA::Page::Project::SubMenus::SuperSidebar::Common - end - end - def go_to_metrics open_monitor_submenu('Metrics') end diff --git a/qa/qa/page/project/sub_menus/super_sidebar/operate.rb b/qa/qa/page/project/sub_menus/super_sidebar/operate.rb index f2a131d915d..5e4306c77d0 100644 --- a/qa/qa/page/project/sub_menus/super_sidebar/operate.rb +++ b/qa/qa/page/project/sub_menus/super_sidebar/operate.rb @@ -8,14 +8,6 @@ module QA module Operate extend QA::Page::PageConcern - def self.included(base) - super - - base.class_eval do - include QA::Page::Project::SubMenus::SuperSidebar::Common - end - end - def go_to_package_registry open_operate_submenu('Package Registry') end diff --git a/qa/qa/page/project/sub_menus/super_sidebar/plan.rb b/qa/qa/page/project/sub_menus/super_sidebar/plan.rb index 4ed9faeffc0..ca81837b2e2 100644 --- a/qa/qa/page/project/sub_menus/super_sidebar/plan.rb +++ b/qa/qa/page/project/sub_menus/super_sidebar/plan.rb @@ -6,32 +6,16 @@ module QA module SubMenus module SuperSidebar module Plan - extend QA::Page::PageConcern - def self.included(base) super base.class_eval do - include QA::Page::Project::SubMenus::SuperSidebar::Common + include QA::Page::SubMenus::SuperSidebar::Plan end end - def go_to_boards - open_plan_submenu("Issue boards") - end - - def go_to_service_desk - open_plan_submenu("Service Desk") - end - - def go_to_wiki - open_plan_submenu("Wiki") - end - - private - - def open_plan_submenu(sub_menu) - open_submenu("Plan", "#plan", sub_menu) + def go_to_requirements + open_plan_submenu("Requirements") end end end diff --git a/qa/qa/page/project/sub_menus/super_sidebar/project.rb b/qa/qa/page/project/sub_menus/super_sidebar/project.rb index ef3b8cc9596..0d2df959548 100644 --- a/qa/qa/page/project/sub_menus/super_sidebar/project.rb +++ b/qa/qa/page/project/sub_menus/super_sidebar/project.rb @@ -8,14 +8,6 @@ module QA module Project extend QA::Page::PageConcern - def self.included(base) - super - - base.class_eval do - include QA::Page::Project::SubMenus::Common - end - end - def click_project within_sidebar do click_element(:sidebar_menu_link, menu_item: 'Project overview') diff --git a/qa/qa/page/project/sub_menus/super_sidebar/secure.rb b/qa/qa/page/project/sub_menus/super_sidebar/secure.rb index 4fea95a5456..1b9e35d3d37 100644 --- a/qa/qa/page/project/sub_menus/super_sidebar/secure.rb +++ b/qa/qa/page/project/sub_menus/super_sidebar/secure.rb @@ -8,18 +8,6 @@ module QA module Secure extend QA::Page::PageConcern - def self.included(base) - super - - base.class_eval do - include QA::Page::Project::SubMenus::SuperSidebar::Common - end - end - - def go_to_audit_events - open_secure_submenu('Audit events') - end - def go_to_security_configuration open_secure_submenu('Security configuration') end diff --git a/qa/qa/page/project/sub_menus/super_sidebar/settings.rb b/qa/qa/page/project/sub_menus/super_sidebar/settings.rb index eead4f9b147..f2833239966 100644 --- a/qa/qa/page/project/sub_menus/super_sidebar/settings.rb +++ b/qa/qa/page/project/sub_menus/super_sidebar/settings.rb @@ -8,14 +8,6 @@ module QA module Settings extend QA::Page::PageConcern - def self.included(base) - super - - base.class_eval do - include QA::Page::Project::SubMenus::SuperSidebar::Common - end - end - def go_to_general_settings open_settings_submenu('General') end diff --git a/qa/qa/page/sub_menus/common.rb b/qa/qa/page/sub_menus/common.rb index 518b3b4e84e..2e3e53ac793 100644 --- a/qa/qa/page/sub_menus/common.rb +++ b/qa/qa/page/sub_menus/common.rb @@ -15,38 +15,41 @@ module QA end end - def within_sidebar + def within_sidebar(&block) wait_for_requests - within_element(sidebar_element) do - yield - end + within_element(sidebar_element, &block) end - def within_submenu(element = nil) + def within_submenu(element = nil, &block) if element - within_element(element) do - yield - end + within_element(element, &block) else - within_submenu_without_element do - yield - end + within_submenu_without_element(&block) end end private - def within_submenu_without_element - if has_css?('.fly-out-list') - within('.fly-out-list') do - yield - end - else - yield + # Implementation for super-sidebar, will replace within_submenu + # + # @param [String] parent_menu_name + # @param [String] parent_section_id + # @param [String] sub_menu + # @return [void] + def open_submenu(parent_menu_name, parent_section_id, sub_menu) + click_element(:sidebar_menu_link, menu_item: parent_menu_name) + + # TODO: it's not possible to add qa-selectors to sub-menu container + within(parent_section_id) do + click_element(:sidebar_menu_link, menu_item: sub_menu) end end + def within_submenu_without_element(&block) + has_css?('.fly-out-list') ? within('.fly-out-list', &block) : yield + end + def sidebar_element raise NotImplementedError end diff --git a/qa/qa/page/sub_menus/super_sidebar/manage.rb b/qa/qa/page/sub_menus/super_sidebar/manage.rb new file mode 100644 index 00000000000..ddab1b373cf --- /dev/null +++ b/qa/qa/page/sub_menus/super_sidebar/manage.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module QA + module Page + module SubMenus + module SuperSidebar + module Manage + extend QA::Page::PageConcern + + def self.included(base) + super + + base.class_eval do + include QA::Page::SubMenus::Common + end + end + + def go_to_activity + open_manage_submenu('Activity') + end + + def go_to_members + open_manage_submenu('Members') + end + + def go_to_labels + open_manage_submenu('Labels') + end + + def go_to_milestones + open_manage_submenu('Milestones') + end + + private + + def open_manage_submenu(sub_menu) + open_submenu('Manage', '#manage', sub_menu) + end + end + end + end + end +end diff --git a/qa/qa/page/sub_menus/super_sidebar/plan.rb b/qa/qa/page/sub_menus/super_sidebar/plan.rb new file mode 100644 index 00000000000..7e966450e19 --- /dev/null +++ b/qa/qa/page/sub_menus/super_sidebar/plan.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module QA + module Page + module SubMenus + module SuperSidebar + module Plan + extend QA::Page::PageConcern + + def self.included(base) + super + + base.class_eval do + include QA::Page::SubMenus::Common + end + end + + def go_to_issue_boards + open_plan_submenu("Issue boards") + end + + def go_to_service_desk + open_plan_submenu("Service Desk") + end + + def go_to_wiki + open_plan_submenu("Wiki") + end + + private + + def open_plan_submenu(sub_menu) + open_submenu("Plan", "#plan", sub_menu) + end + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue_boards/focus_mode_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue_boards/focus_mode_spec.rb index 83e178ae4c3..7377b0ff8af 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue_boards/focus_mode_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue_boards/focus_mode_spec.rb @@ -16,7 +16,7 @@ module QA it 'focuses on issue board', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347996' do project.visit! - Page::Project::Menu.perform(&:go_to_boards) + Page::Project::Menu.perform(&:go_to_issue_boards) Page::Component::IssueBoard::Show.perform do |show| show.click_focus_mode_button diff --git a/spec/factories/work_items/resource_link_events.rb b/spec/factories/work_items/resource_link_events.rb new file mode 100644 index 00000000000..696f6dcc43f --- /dev/null +++ b/spec/factories/work_items/resource_link_events.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :resource_link_event, class: 'WorkItems::ResourceLinkEvent' do + action { :add } + issue { association(:issue) } + user { issue&.author || association(:user) } + child_work_item { association(:work_item, :task) } + end +end diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js index e078d5a943c..f62104f4622 100644 --- a/spec/frontend/super_sidebar/components/user_bar_spec.js +++ b/spec/frontend/super_sidebar/components/user_bar_spec.js @@ -25,6 +25,7 @@ describe('UserBar component', () => { const findBrandLogo = () => wrapper.findByTestId('brand-header-custom-logo'); const findSearchButton = () => wrapper.findByTestId('super-sidebar-search-button'); const findSearchModal = () => wrapper.findComponent(SearchModal); + const findStopImpersonationButton = () => wrapper.findByTestId('stop-impersonation-btn'); Vue.use(Vuex); @@ -33,7 +34,7 @@ describe('UserBar component', () => { searchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS, }, }); - const createWrapper = (extraSidebarData = {}) => { + const createWrapper = ({ extraSidebarData = {}, provideOverrides = {} } = {}) => { wrapper = shallowMountExtended(UserBar, { propsData: { sidebarData: { ...sidebarData, ...extraSidebarData }, @@ -41,6 +42,8 @@ describe('UserBar component', () => { provide: { rootPath: '/', toggleNewNavEndpoint: '/-/profile/preferences', + isImpersonating: false, + ...provideOverrides, }, directives: { GlTooltip: createMockDirective('gl-tooltip'), @@ -95,12 +98,16 @@ describe('UserBar component', () => { expect(findBrandLogo().exists()).toBe(true); expect(findBrandLogo().attributes('src')).toBe(sidebarData.logo_url); }); + + it('does not render the "Stop impersonating" button', () => { + expect(findStopImpersonationButton().exists()).toBe(false); + }); }); describe('GitLab Next badge', () => { describe('when on canary', () => { it('should render a badge to switch off GitLab Next', () => { - createWrapper({ gitlab_com_and_canary: true }); + createWrapper({ extraSidebarData: { gitlab_com_and_canary: true } }); const badge = wrapper.findComponent(GlBadge); expect(badge.text()).toBe('Next'); expect(badge.attributes('href')).toBe(sidebarData.canary_toggle_com_url); @@ -109,7 +116,7 @@ describe('UserBar component', () => { describe('when not on canary', () => { it('should not render the GitLab Next badge', () => { - createWrapper({ gitlab_com_and_canary: false }); + createWrapper({ extraSidebarData: { gitlab_com_and_canary: false } }); const badge = wrapper.findComponent(GlBadge); expect(badge.exists()).toBe(false); }); @@ -135,4 +142,29 @@ describe('UserBar component', () => { expect(findSearchModal().exists()).toBe(true); }); }); + + describe('While impersonating a user', () => { + beforeEach(() => { + createWrapper({ provideOverrides: { isImpersonating: true } }); + }); + + it('renders the "Stop impersonating" button', () => { + expect(findStopImpersonationButton().exists()).toBe(true); + }); + + it('sets the correct label on the button', () => { + const btn = findStopImpersonationButton(); + const label = __('Stop impersonating'); + + expect(btn.attributes('title')).toBe(label); + expect(btn.attributes('aria-label')).toBe(label); + }); + + it('sets the href and data-method attributes', () => { + const btn = findStopImpersonationButton(); + + expect(btn.attributes('href')).toBe(sidebarData.stop_impersonation_path); + expect(btn.attributes('data-method')).toBe('delete'); + }); + }); }); diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js index fdf88e910e3..1d8f595a400 100644 --- a/spec/frontend/super_sidebar/mock_data.js +++ b/spec/frontend/super_sidebar/mock_data.js @@ -103,6 +103,7 @@ export const sidebarData = { pinned_items: [], panel_type: 'your_work', update_pins_url: 'path/to/pins', + stop_impersonation_path: '/admin/impersonation', }; export const userMenuMockStatus = { diff --git a/spec/frontend/vue_shared/components/entity_select/project_select_spec.js b/spec/frontend/vue_shared/components/entity_select/project_select_spec.js index 32ce2155494..0a174c98efb 100644 --- a/spec/frontend/vue_shared/components/entity_select/project_select_spec.js +++ b/spec/frontend/vue_shared/components/entity_select/project_select_spec.js @@ -97,6 +97,7 @@ describe('ProjectSelect', () => { ${'defaultToggleText'} | ${PROJECT_TOGGLE_TEXT} ${'headerText'} | ${PROJECT_HEADER_TEXT} ${'clearable'} | ${true} + ${'block'} | ${false} `('passes the $prop prop to entity-select', ({ prop, expectedValue }) => { expect(findEntitySelect().props(prop)).toBe(expectedValue); }); @@ -136,6 +137,18 @@ describe('ProjectSelect', () => { expect(mock.history.get[0].params.include_subgroups).toBe(true); }); + it('does not include shared projects if withShared prop is false', async () => { + createComponent({ + props: { + withShared: false, + }, + }); + openListbox(); + await waitForPromises(); + + expect(mock.history.get[0].params.with_shared).toBe(false); + }); + it('fetches projects globally if no group ID is provided', async () => { createComponent({ props: { diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb index 76e87667e25..30357aef601 100644 --- a/spec/helpers/sidebars_helper_spec.rb +++ b/spec/helpers/sidebars_helper_spec.rb @@ -331,6 +331,23 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do end end end + + describe 'impersonation data' do + it 'sets is_impersonating to `false` when not impersonating' do + expect(subject[:is_impersonating]).to be(false) + end + + it 'passes the stop_impersonation_path property' do + expect(subject[:stop_impersonation_path]).to eq(admin_impersonation_path) + end + + describe 'when impersonating' do + it 'sets is_impersonating to `true`' do + expect(helper).to receive(:session).and_return({ impersonator_id: 1 }) + expect(subject[:is_impersonating]).to be(true) + end + end + end end describe '#super_sidebar_nav_panel' do diff --git a/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb b/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb index 42a43392f9f..f4b13033270 100644 --- a/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb +++ b/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Database::Partitioning::ConvertTableToFirstListPartition do +RSpec.describe Gitlab::Database::Partitioning::ConvertTableToFirstListPartition, feature_category: :database do include Gitlab::Database::DynamicModelHelpers include Database::TableSchemaHelpers @@ -77,7 +77,9 @@ RSpec.describe Gitlab::Database::Partitioning::ConvertTableToFirstListPartition end describe "#prepare_for_partitioning" do - subject(:prepare) { converter.prepare_for_partitioning } + subject(:prepare) { converter.prepare_for_partitioning(async: async) } + + let(:async) { false } it 'adds a check constraint' do expect { prepare }.to change { @@ -87,9 +89,100 @@ RSpec.describe Gitlab::Database::Partitioning::ConvertTableToFirstListPartition .count }.from(0).to(1) end + + context 'when it fails to add constraint' do + before do + allow(migration_context).to receive(:add_check_constraint) + end + + it 'raises UnableToPartition error' do + expect { prepare } + .to raise_error(described_class::UnableToPartition) + .and change { + Gitlab::Database::PostgresConstraint + .check_constraints + .by_table_identifier(table_identifier) + .count + }.by(0) + end + end + + context 'when async' do + let(:async) { true } + + it 'adds a NOT VALID check constraint' do + expect { prepare }.to change { + Gitlab::Database::PostgresConstraint + .check_constraints + .by_table_identifier(table_identifier) + .count + }.from(0).to(1) + + constraint = + Gitlab::Database::PostgresConstraint + .check_constraints + .by_table_identifier(table_identifier) + .last + + expect(constraint.definition).to end_with('NOT VALID') + end + + it 'adds a PostgresAsyncConstraintValidation record' do + expect { prepare }.to change { + Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValidation.count + }.from(0).to(1) + + record = Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValidation.last + expect(record.name).to eq described_class::PARTITIONING_CONSTRAINT_NAME + expect(record).to be_check_constraint + end + + context 'when constraint exists but is not valid' do + before do + converter.prepare_for_partitioning(async: true) + end + + it 'validates the check constraint' do + expect { prepare }.to change { + Gitlab::Database::PostgresConstraint + .check_constraints + .by_table_identifier(table_identifier).first.constraint_valid? + }.from(false).to(true) + end + + context 'when it fails to validate constraint' do + before do + allow(migration_context).to receive(:validate_check_constraint) + end + + it 'raises UnableToPartition error' do + expect { prepare } + .to raise_error(described_class::UnableToPartition, + starting_with('Error validating partitioning constraint')) + .and change { + Gitlab::Database::PostgresConstraint + .check_constraints + .by_table_identifier(table_identifier) + .count + }.by(0) + end + end + end + + context 'when constraint exists and is valid' do + before do + converter.prepare_for_partitioning(async: false) + end + + it 'raises UnableToPartition error' do + expect(Gitlab::AppLogger).to receive(:info).with(starting_with('Nothing to do')) + prepare + end + end + end end - describe '#revert_prepare_for_partitioning' do + describe '#revert_preparation_for_partitioning' do before do converter.prepare_for_partitioning end @@ -109,8 +202,10 @@ RSpec.describe Gitlab::Database::Partitioning::ConvertTableToFirstListPartition describe "#partition" do subject(:partition) { converter.partition } + let(:async) { false } + before do - converter.prepare_for_partitioning + converter.prepare_for_partitioning(async: async) end context 'when the primary key is incorrect' do @@ -134,7 +229,15 @@ RSpec.describe Gitlab::Database::Partitioning::ConvertTableToFirstListPartition end it 'throws a reasonable error message' do - expect { partition }.to raise_error(described_class::UnableToPartition, /constraint /) + expect { partition }.to raise_error(described_class::UnableToPartition, /is not ready for partitioning./) + end + end + + context 'when supporting check constraint is not valid' do + let(:async) { true } + + it 'throws a reasonable error message' do + expect { partition }.to raise_error(described_class::UnableToPartition, /is not ready for partitioning./) end end @@ -207,7 +310,7 @@ RSpec.describe Gitlab::Database::Partitioning::ConvertTableToFirstListPartition proc do allow(migration_context.connection).to receive(:add_foreign_key).and_call_original expect(migration_context.connection).to receive(:add_foreign_key).with(from_table, to_table, any_args) - .and_wrap_original(&fail_first_time) + .and_wrap_original(&fail_first_time) end end diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb index e76b1da3834..06c40f18b43 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHelpers do +RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHelpers, feature_category: :database do include Database::PartitioningHelpers include Database::TriggerHelpers include Database::TableSchemaHelpers @@ -98,7 +98,8 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe migration.prepare_constraint_for_list_partitioning(table_name: source_table, partitioning_column: partition_column, parent_table_name: partitioned_table, - initial_partitioning_value: min_date) + initial_partitioning_value: min_date, + async: false) end end end diff --git a/spec/models/work_items/resource_link_event_spec.rb b/spec/models/work_items/resource_link_event_spec.rb new file mode 100644 index 00000000000..67ca9e72bbc --- /dev/null +++ b/spec/models/work_items/resource_link_event_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WorkItems::ResourceLinkEvent, type: :model, feature_category: :team_planning do + it_behaves_like 'a resource event' + + describe 'associations' do + it { is_expected.to belong_to(:work_item) } + it { is_expected.to belong_to(:child_work_item) } + end + + describe 'validation' do + it { is_expected.to validate_presence_of(:child_work_item) } + end +end diff --git a/spec/support/shared_examples/models/resource_event_shared_examples.rb b/spec/support/shared_examples/models/resource_event_shared_examples.rb index 038ff33c68a..1409f7caea8 100644 --- a/spec/support/shared_examples/models/resource_event_shared_examples.rb +++ b/spec/support/shared_examples/models/resource_event_shared_examples.rb @@ -10,6 +10,8 @@ RSpec.shared_examples 'a resource event' do let_it_be(:issue2) { create(:issue, author: user1) } let_it_be(:issue3) { create(:issue, author: user2) } + let(:resource_event) { described_class.name.demodulize.underscore.to_sym } + describe 'importable' do it { is_expected.to respond_to(:importing?) } it { is_expected.to respond_to(:imported?) } @@ -36,9 +38,9 @@ RSpec.shared_examples 'a resource event' do let!(:created_at2) { 2.days.ago } let!(:created_at3) { 3.days.ago } - let!(:event1) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: created_at1) } - let!(:event2) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: created_at2) } - let!(:event3) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: created_at3) } + let!(:event1) { create(resource_event, issue: issue1, created_at: created_at1) } + let!(:event2) { create(resource_event, issue: issue2, created_at: created_at2) } + let!(:event3) { create(resource_event, issue: issue2, created_at: created_at3) } it 'returns the expected events' do events = described_class.created_after(created_at3) @@ -62,9 +64,10 @@ RSpec.shared_examples 'a resource event for issues' do let_it_be(:issue2) { create(:issue, author: user1) } let_it_be(:issue3) { create(:issue, author: user2) } - let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue1) } - let_it_be(:event2) { create(described_class.name.underscore.to_sym, issue: issue2) } - let_it_be(:event3) { create(described_class.name.underscore.to_sym, issue: issue1) } + let_it_be(:resource_event) { described_class.name.demodulize.underscore.to_sym } + let_it_be(:event1) { create(resource_event, issue: issue1) } + let_it_be(:event2) { create(resource_event, issue: issue2) } + let_it_be(:event3) { create(resource_event, issue: issue1) } describe 'associations' do it { is_expected.to belong_to(:issue) } @@ -93,9 +96,9 @@ RSpec.shared_examples 'a resource event for issues' do end describe '.by_created_at_earlier_or_equal_to' do - let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-10') } - let_it_be(:event2) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: '2020-03-10') } - let_it_be(:event3) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-12') } + let_it_be(:event1) { create(resource_event, issue: issue1, created_at: '2020-03-10') } + let_it_be(:event2) { create(resource_event, issue: issue2, created_at: '2020-03-10') } + let_it_be(:event3) { create(resource_event, issue: issue1, created_at: '2020-03-12') } it 'returns the expected events' do events = described_class.by_created_at_earlier_or_equal_to('2020-03-11 23:59:59') @@ -112,7 +115,7 @@ RSpec.shared_examples 'a resource event for issues' do if described_class.method_defined?(:issuable) describe '#issuable' do - let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue2) } + let_it_be(:event1) { create(resource_event, issue: issue2) } it 'returns the expected issuable' do expect(event1.issuable).to eq(issue2) @@ -125,6 +128,7 @@ RSpec.shared_examples 'a resource event for merge requests' do let_it_be(:user1) { create(:user) } let_it_be(:user2) { create(:user) } + let_it_be(:resource_event) { described_class.name.demodulize.underscore.to_sym } let_it_be(:merge_request1) { create(:merge_request, author: user1) } let_it_be(:merge_request2) { create(:merge_request, author: user1) } let_it_be(:merge_request3) { create(:merge_request, author: user2) } @@ -134,9 +138,9 @@ RSpec.shared_examples 'a resource event for merge requests' do end describe '.by_merge_request' do - let_it_be(:event1) { create(described_class.name.underscore.to_sym, merge_request: merge_request1) } - let_it_be(:event2) { create(described_class.name.underscore.to_sym, merge_request: merge_request2) } - let_it_be(:event3) { create(described_class.name.underscore.to_sym, merge_request: merge_request1) } + let_it_be(:event1) { create(resource_event, merge_request: merge_request1) } + let_it_be(:event2) { create(resource_event, merge_request: merge_request2) } + let_it_be(:event3) { create(resource_event, merge_request: merge_request1) } it 'returns the expected records for an issue with events' do events = described_class.by_merge_request(merge_request1) @@ -153,7 +157,7 @@ RSpec.shared_examples 'a resource event for merge requests' do if described_class.method_defined?(:issuable) describe '#issuable' do - let_it_be(:event1) { create(described_class.name.underscore.to_sym, merge_request: merge_request2) } + let_it_be(:event1) { create(resource_event, merge_request: merge_request2) } it 'returns the expected issuable' do expect(event1.issuable).to eq(merge_request2) @@ -163,7 +167,7 @@ RSpec.shared_examples 'a resource event for merge requests' do context 'on callbacks' do it 'does not trigger note created subscription' do - event = build(described_class.name.underscore.to_sym, merge_request: merge_request1) + event = build(resource_event, merge_request: merge_request1) expect(GraphqlTriggers).not_to receive(:work_item_note_created) expect(event).not_to receive(:trigger_note_subscription_create) @@ -177,15 +181,17 @@ RSpec.shared_examples 'a note for work item resource event' do let_it_be(:project) { create(:project) } let_it_be(:work_item) { create(:work_item, :task, project: project, author: user) } + let(:resource_event) { described_class.name.demodulize.underscore.to_sym } + it 'builds synthetic note with correct synthetic_note_class' do - event = build(described_class.name.underscore.to_sym, issue: work_item) + event = build(resource_event, issue: work_item) expect(event.work_item_synthetic_system_note.class.name).to eq(event.synthetic_note_class.name) end context 'on callbacks' do it 'triggers note created subscription' do - event = build(described_class.name.underscore.to_sym, issue: work_item) + event = build(resource_event, issue: work_item) expect(GraphqlTriggers).to receive(:work_item_note_created) expect(event).to receive(:trigger_note_subscription_create).and_call_original |