diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb index 533d91089883ab6b49dfce075c22737f5e042ac2..f584f30ca96342ab5f0970cf58dd86850da40169 100644 --- a/app/models/bulk_import.rb +++ b/app/models/bulk_import.rb @@ -121,6 +121,23 @@ def parent_group_entity entities.group_entity.where(parent: nil).first end + def destination_group_roots + entities.where(parent: nil).filter_map do |entity| + entity.group || entity.project + end.map(&:root_ancestor).uniq + end + + def namespaces_with_unassigned_placeholders + namespaces = destination_group_roots + namespace_ids = namespaces.collect(&:id) + + reassignable_statuses = Import::SourceUser::STATUSES.slice(*Import::SourceUser::REASSIGNABLE_STATUSES).values + source_users = Import::SourceUser.for_namespace(namespace_ids).by_statuses(reassignable_statuses) + valid_namespace_ids = source_users.collect(&:namespace_id).uniq + + namespaces.select { |namespace| valid_namespace_ids.include?(namespace.id) } + end + def source_url configuration&.url end diff --git a/app/presenters/import/pending_reassignment_alert_presenter.rb b/app/presenters/import/pending_reassignment_alert_presenter.rb new file mode 100644 index 0000000000000000000000000000000000000000..1462867269c110770de6ff583160101db2c1f0b4 --- /dev/null +++ b/app/presenters/import/pending_reassignment_alert_presenter.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Import + class PendingReassignmentAlertPresenter < Gitlab::View::Presenter::Simple + include Gitlab::Utils::StrongMemoize + include ActionView::Helpers::TagHelper + include SafeFormatHelper + + presents ::BulkImport, as: :bulk_import + + def show_alert? + Feature.enabled?(:importer_user_mapping, current_user) && groups_awaiting_placeholder_assignment.any? + end + + def groups_awaiting_placeholder_assignment + return [] unless bulk_import + + namespaces = bulk_import.namespaces_with_unassigned_placeholders + namespaces.select do |namespace| + namespace.owners.include?(current_user) + end + end + strong_memoize_attr :groups_awaiting_placeholder_assignment + + def group_names + return '' if groups_awaiting_placeholder_assignment.empty? + + groups_awaiting_placeholder_assignment.collect(&:name).to_sentence + end + + def source_hostname + bulk_import.configuration.source_hostname + end + + def title + s_('UserMapping|Placeholder users awaiting reassignment') + end + + def body + safe_format( + s_('UserMapping|As part of the import, placeholder users were created on ' \ + '%{group_names} and these users were assigned group membership and ' \ + 'contributions from %{source_hostname}. To reassign contributions from ' \ + 'placeholder users to GitLab users, visit the Members page of %{group_links}.'), + group_names: group_names, + source_hostname: source_hostname, + group_links: group_links + ) + end + + def group_links + placeholders = [] + tag_pairs = [] + + groups_awaiting_placeholder_assignment.collect do |namespace| + placeholders << "%{group_#{namespace.id}_link_start}#{namespace.name}%{group_#{namespace.id}_link_end}" + tag_pairs << tag_pair( + tag.a(href: group_group_members_path(namespace)), + :"group_#{namespace.id}_link_start", + :"group_#{namespace.id}_link_end" + ) + end + + safe_format(placeholders.to_sentence, *tag_pairs) + end + end +end diff --git a/app/views/import/bulk_imports/history.html.haml b/app/views/import/bulk_imports/history.html.haml index 81eb08cfbc59278bd6d015bcc07f074061bb5649..13a88867d5a1763f5fedbc2223750d5590e3aa1c 100644 --- a/app/views/import/bulk_imports/history.html.haml +++ b/app/views/import/bulk_imports/history.html.haml @@ -7,4 +7,13 @@ - add_page_specific_style 'page_bundles/import' +- pending_reassignment_presenter = Gitlab::View::Presenter::Factory.new(@bulk_import, current_user: current_user, presenter_class: ::Import::PendingReassignmentAlertPresenter).fabricate! +- if pending_reassignment_presenter.show_alert? + = render Pajamas::AlertComponent.new(variant: :notice, title: pending_reassignment_presenter.title, alert_options: { class: 'gl-mt-4' }) do |c| + - c.with_body do + = pending_reassignment_presenter.body + - c.with_actions do + = render Pajamas::ButtonComponent.new(variant: :default, href: help_page_path('user/project/import/index', anchor: 'placeholder-users'), button_options: { class: 'deferred-link gl-alert-action', rel: 'noreferrer noopener' }, target: '_blank') do + = _('Learn more') + #import-history-mount-element{ data: { id: @bulk_import&.id, details_path: failures_import_bulk_import_path(':id', ':entity_id'), realtime_changes_path: realtime_changes_import_bulk_imports_path(format: :json) } } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1e2ea1f4b40f1dd09bc4747bd6dd060ec9f15607..024571676b34dee76d5523d6eb2d775dd8a4b539 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -58450,6 +58450,9 @@ msgstr "" msgid "UserMapping|Approve reassignment" msgstr "" +msgid "UserMapping|As part of the import, placeholder users were created on %{group_names} and these users were assigned group membership and contributions from %{source_hostname}. To reassign contributions from placeholder users to GitLab users, visit the Members page of %{group_links}." +msgstr "" + msgid "UserMapping|Awaiting reassignment" msgstr "" @@ -58501,6 +58504,9 @@ msgstr "" msgid "UserMapping|Placeholder user was made permanent." msgstr "" +msgid "UserMapping|Placeholder users awaiting reassignment" +msgstr "" + msgid "UserMapping|Placeholders" msgstr "" diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb index 7a5a69ea5b55a0c4b9c039084d59b9d938920446..2909d451af9bb760e302a0ecb30cdb5135d4e37c 100644 --- a/spec/helpers/import_helper_spec.rb +++ b/spec/helpers/import_helper_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ImportHelper do +RSpec.describe ImportHelper, feature_category: :importers do describe '#sanitize_project_name' do it 'removes leading tildes' do expect(helper.sanitize_project_name('~~root')).to eq('root') diff --git a/spec/models/bulk_import_spec.rb b/spec/models/bulk_import_spec.rb index 5870edfb25ca8c5bec8f6b45c2506e486b31c4b5..1d8759ea8282b8cca38f4606f98ce53a58dd78c4 100644 --- a/spec/models/bulk_import_spec.rb +++ b/spec/models/bulk_import_spec.rb @@ -210,11 +210,32 @@ def supports_block_expectations? let_it_be(:root_node) { create(:bulk_import_entity) } - it 'returns the topmost group note of the import entity tree' do + it 'returns the topmost node of the first tree of the import entity structure' do expect(import.parent_group_entity).to eq(root_node) end end + describe '#destination_group_roots' do + subject(:import) do + create(:bulk_import, :started, entities: [ + root_project_entity, + root_group_entity, + create(:bulk_import_entity, parent: root_group_entity) + ]) + end + + let_it_be(:project_namespace) { create(:group) } + let_it_be(:project) { create(:project, namespace: project_namespace) } + let_it_be(:root_project_entity) { create(:bulk_import_entity, :project_entity, project: project) } + + let_it_be(:top_level_group) { create(:group) } + let_it_be(:root_group_entity) { create(:bulk_import_entity, :group_entity, group: top_level_group) } + + it 'returns the topmost group nodes of the import entity tree' do + expect(import.destination_group_roots).to match_array([project_namespace, top_level_group]) + end + end + describe '#source_url' do it 'returns migration source url via configuration' do import = create(:bulk_import, :with_configuration) @@ -230,4 +251,27 @@ def supports_block_expectations? end end end + + describe '#namespaces_with_unassigned_placeholders' do + let_it_be(:group) { create(:group) } + let_it_be(:entity) do + create(:bulk_import_entity, :group_entity, bulk_import: finished_bulk_import, group: group) + end + + before do + create_list(:import_source_user, 5, :completed, namespace: group) + end + + context 'when all placeholders have been assigned' do + it { expect(finished_bulk_import.namespaces_with_unassigned_placeholders).to be_empty } + end + + context 'when some placeholders have not been assigned' do + before do + create(:import_source_user, :pending_reassignment, namespace: group) + end + + it { expect(finished_bulk_import.namespaces_with_unassigned_placeholders).to include(group) } + end + end end diff --git a/spec/presenters/import/pending_reassignment_alert_presenter_spec.rb b/spec/presenters/import/pending_reassignment_alert_presenter_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a48e67f8da57a123717195a67ef7b995a7a6961d --- /dev/null +++ b/spec/presenters/import/pending_reassignment_alert_presenter_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Import::PendingReassignmentAlertPresenter, :aggregate_failures, feature_category: :importers do + include SafeFormatHelper + + let_it_be(:user) { build_stubbed(:user) } + let_it_be(:bulk_import) { build_stubbed(:bulk_import, :with_configuration) } + let(:presenter) { described_class.new(bulk_import, current_user: user) } + let_it_be(:namespaces) { [] } + + before do + namespaces.each do |namespace| + allow(namespace).to receive(:owners).and_return([user]) + end + + allow(bulk_import).to receive(:namespaces_with_unassigned_placeholders).and_return(namespaces) + end + + describe '#title' do + subject { presenter.title } + + it { is_expected.to eq(s_('UserMapping|Placeholder users awaiting reassignment')) } + end + + describe '#body' do + subject { presenter.body } + + it do + is_expected.to eq( + safe_format( + s_('UserMapping|As part of the import, placeholder users were created on ' \ + '%{group_names} and these users were assigned group membership and ' \ + 'contributions from %{source_hostname}. To reassign contributions from ' \ + 'placeholder users to GitLab users, visit the Members page of %{group_links}.'), + group_names: '', + source_hostname: 'gitlab.example', + group_links: '' + )) + end + end + + context 'with no top level groups' do + let_it_be(:namespaces) { [] } + + it 'presents the import values' do + expect(presenter.show_alert?).to eq(false) + end + end + + context 'with one top level group' do + let_it_be(:namespaces) do + source_users = build_stubbed(:import_source_user) + [build_stubbed(:group, id: 1, name: 'blink', import_source_users: [source_users])] + end + + it 'presents the import values' do + expect(presenter.show_alert?).to eq(true) + expect(presenter.group_links).to eq("blink") + expect(presenter.groups_awaiting_placeholder_assignment).to match_array(namespaces) + expect(presenter.group_names).to eq('blink') + expect(presenter.source_hostname).to eq('gitlab.example') + end + + context 'when importer_user_mapping feature flag is disabled' do + before do + stub_feature_flags(importer_user_mapping: false) + end + + it 'presents the import values' do + expect(presenter.show_alert?).to eq(false) + end + end + end + + context 'with multiple top level groups' do + let_it_be(:namespaces) do + [ + build_stubbed(:group, id: 1, name: 'blink'), + build_stubbed(:group, id: 3, name: 'marquee'), + build_stubbed(:group, id: 7, name: 'details') + ] + end + + it 'presents the import values' do + expect(presenter.show_alert?).to eq(true) + expect(presenter.group_links).to eq( + "blink, " \ + "marquee, " \ + "and details" + ) + expect(presenter.groups_awaiting_placeholder_assignment).to match_array(namespaces) + expect(presenter.group_names).to eq('blink, marquee, and details') + expect(presenter.source_hostname).to eq('gitlab.example') + end + + context 'when the current user is not an owner of a top level group' do + it 'excludes that group from the results' do + allow(namespaces[0]).to receive(:owners).and_return([]) + + expect(presenter.groups_awaiting_placeholder_assignment).to match_array(namespaces[1..]) + end + end + end +end