diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index d655777224170a9596b751e8403a0a9d39e1f938..b7299df1bc137aa47ef8f1f4a68b437b091a7eb1 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -16,6 +16,7 @@ = render "shared/service_ping_consent" = render_two_factor_auth_recovery_settings_check = render_if_exists "layouts/header/ee_subscribable_banner" + = render_if_exists "layouts/header/seats_count_alert" = render_if_exists "shared/namespace_storage_limit_alert" = render_if_exists "shared/namespace_user_cap_reached_alert" = render_if_exists "shared/new_user_signups_cap_reached_alert" diff --git a/ee/app/helpers/seats_count_alert_helper.rb b/ee/app/helpers/seats_count_alert_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..33b42eeb7d67d2ae90f45696ddfb96a3ce724c75 --- /dev/null +++ b/ee/app/helpers/seats_count_alert_helper.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module SeatsCountAlertHelper + def display_seats_count_alert! + @display_seats_count_alert = true + end + + def learn_more_link + link_to _('Learn more.'), help_page_path('subscriptions/quarterly_reconciliation'), target: '_blank', rel: 'noopener noreferrer' + end + + def group_name + root_namespace&.name + end + + def remaining_seats_count + return unless total_seats_count && seats_in_use + + total_seats_count - seats_in_use + end + + def seats_usage_link + return unless root_namespace + + link_to _('View seat usage'), current_usage_quotas_path, class: 'btn gl-alert-action btn-info btn-md gl-button' + end + + def show_seats_count_alert? + return false unless root_namespace&.group_namespace? + return false unless root_namespace&.has_owner?(current_user) + return false unless current_subscription + + !!@display_seats_count_alert + end + + def total_seats_count + current_subscription&.seats + end + + private + + def root_namespace + @project&.root_ancestor || @group&.root_ancestor + end + + def current_subscription + root_namespace&.gitlab_subscription + end + + def seats_in_use + current_subscription&.seats_in_use + end + + def current_usage_quotas_path + usage_quotas_path(root_namespace, anchor: 'seats-quota-tab') + end +end diff --git a/ee/app/views/layouts/header/_seats_count_alert.html.haml b/ee/app/views/layouts/header/_seats_count_alert.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..3bd36b43a296d16a5e8e49e418d35df78db52816 --- /dev/null +++ b/ee/app/views/layouts/header/_seats_count_alert.html.haml @@ -0,0 +1,12 @@ +- return unless show_seats_count_alert? +.container.container-limited.pt-3 + .gl-alert.gl-alert-info{ role: 'alert' } + = sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon') + %button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') } + = sprite_icon('close', size: 16, css_class: 'gl-icon') + .gl-alert-body + %h4.gl-alert-title= _('%{group_name} is approaching the limit of available seats') % { group_name: group_name } + = _('Your subscription has %{remaining_seats_count} out of %{total_seats_count} seats remaining. Even if you reach the number of seats in your subscription, you can continue to add users, and GitLab will bill you for the overage.') % { remaining_seats_count: remaining_seats_count, total_seats_count: total_seats_count } + = learn_more_link + .gl-alert-actions + = seats_usage_link diff --git a/ee/spec/helpers/seats_count_alert_helper_spec.rb b/ee/spec/helpers/seats_count_alert_helper_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..009918a254182f7e9c3a5446ecefb390b5e180af --- /dev/null +++ b/ee/spec/helpers/seats_count_alert_helper_spec.rb @@ -0,0 +1,188 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SeatsCountAlertHelper, :saas do + include Devise::Test::ControllerHelpers + + let_it_be(:group) { nil } + let_it_be(:project) { nil } + let_it_be(:user) { create(:user) } + + before do + assign(:project, project) + assign(:group, group) + allow(helper).to receive(:current_user).and_return(user) + end + + shared_examples 'learn more link is built' do + it 'builds the correct link' do + expect(helper.learn_more_link).to match %r{.+}m + end + end + + shared_examples 'seats info are not populated' do + it 'sets remaining seats count to nil' do + expect(helper.remaining_seats_count).to be_nil + end + + it 'sets total seats count to nil' do + expect(helper.total_seats_count).to be_nil + end + end + + shared_examples 'seats info are populated' do + it 'sets remaining seats count to the correct number' do + expect(helper.remaining_seats_count).to eq(14) + end + + it 'sets total seats count to the correct number' do + expect(helper.total_seats_count).to eq(15) + end + end + + shared_examples 'group info are populated' do + it 'builds the correct link' do + expect(helper.seats_usage_link).to match %r{.+}m + end + + it 'has a group name' do + expect(helper.group_name).to eq(context.name) + end + end + + shared_examples 'group info are not populated' do + it 'does not build the correct link' do + expect(helper.seats_usage_link).to be_nil + end + + it 'does not have a group name' do + expect(helper.group_name).to be_nil + end + end + + shared_examples 'alert is not displayed while some info are' do + it_behaves_like 'learn more link is built' + + it_behaves_like 'seats info are not populated' + + it_behaves_like 'group info are not populated' + + it 'does not show the alert' do + expect(helper.show_seats_count_alert?).to be false + end + end + + shared_examples 'alert is displayed' do + include_examples 'learn more link is built' + + include_examples 'seats info are populated' + + include_examples 'group info are populated' + + it 'does show the alert' do + expect(helper.show_seats_count_alert?).to be true + end + end + + shared_examples 'alert is not displayed' do + include_examples 'learn more link is built' + + include_examples 'seats info are populated' + + include_examples 'group info are populated' + + it 'does not show the alert' do + expect(helper.show_seats_count_alert?).to be false + end + end + + shared_examples 'common cases for users' do + let_it_be(:gitlab_subscription) do + create(:gitlab_subscription, namespace: context, plan_code: Plan::ULTIMATE, seats: 15, seats_in_use: 1) + end + + describe 'without a owner' do + before do + context.add_user(user, GroupMember::DEVELOPER) + helper.display_seats_count_alert! + end + + include_examples 'alert is not displayed' + end + + describe 'with a owner' do + before do + context.add_owner(user) + end + + context 'without display seats count' do + include_examples 'alert is not displayed' + end + + context 'with display seats count' do + before do + helper.display_seats_count_alert! + end + + include_examples 'alert is displayed' + end + end + end + + it 'sets @display_seats_count_alert to true' do + expect(helper.instance_variable_get(:@display_seats_count_alert)).to be nil + + helper.display_seats_count_alert! + + expect(helper.instance_variable_get(:@display_seats_count_alert)).to be true + end + + describe 'with no subscription' do + include_examples 'alert is not displayed while some info are' + end + + describe 'outside a group or project context' do + before do + helper.display_seats_count_alert! + end + + include_examples 'alert is not displayed while some info are' + end + + describe 'within a group context' do + let_it_be(:group) { create(:group) } + let_it_be(:context) { group } + let_it_be(:project) { nil } + + include_examples 'common cases for users' + end + + describe 'within a subgroup context' do + let_it_be(:context) { create(:group) } + let_it_be(:group) { create(:group, parent: context) } + let_it_be(:project) { nil } + + include_examples 'common cases for users' + end + + describe 'within a project context' do + let_it_be(:group) { nil } + let_it_be(:context) { create(:group) } + let_it_be(:project) { create(:project, namespace: context) } + + include_examples 'common cases for users' + end + + describe 'within a user namespace context' do + let_it_be(:project) { create(:project) } + + before do + helper.display_seats_count_alert! + end + + it 'does show the alert' do + expect(helper.show_seats_count_alert?).to be false + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 08e634d28024e08e21383bf3f82427b23e6d7d70..612f673d5f463272fa4a6f100a1700561701f49d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -632,6 +632,9 @@ msgstr "" msgid "%{group_name} group members" msgstr "" +msgid "%{group_name} is approaching the limit of available seats" +msgstr "" + msgid "%{group_name} uses group managed accounts. You need to create a new GitLab account which will be managed by %{group_name}." msgstr "" @@ -39529,6 +39532,9 @@ msgstr[1] "" msgid "View replaced file @ " msgstr "" +msgid "View seat usage" +msgstr "" + msgid "View setting" msgstr "" @@ -41588,6 +41594,9 @@ msgstr "" msgid "Your subscription expired!" msgstr "" +msgid "Your subscription has %{remaining_seats_count} out of %{total_seats_count} seats remaining. Even if you reach the number of seats in your subscription, you can continue to add users, and GitLab will bill you for the overage." +msgstr "" + msgid "Your subscription is now expired. To renew, export your license usage file and email it to %{renewal_service_email}. A new license will be emailed to the email address registered in the %{customers_dot}. You can upload this license to your instance. To use Free tier, remove your current license." msgstr ""