Commit ba438c94 authored by Doug Stull's avatar Doug Stull Committed by Alper Akgun

Add alert for free plans at limit as a preview

- needed for free user cap project
parent 56c060a8
......@@ -12,6 +12,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-security-newsletter-callout',
'.js-approaching-seats-count-threshold',
'.js-storage-enforcement-banner',
'.js-user-over-limit-free-plan-alert',
];
const initCallouts = () => {
......
......@@ -31,3 +31,5 @@ module Users
end
end
end
Users::GroupCalloutsHelper.prepend_mod
......@@ -14,7 +14,8 @@ module Users
storage_enforcement_banner_first_enforcement_threshold: 3,
storage_enforcement_banner_second_enforcement_threshold: 4,
storage_enforcement_banner_third_enforcement_threshold: 5,
storage_enforcement_banner_fourth_enforcement_threshold: 6
storage_enforcement_banner_fourth_enforcement_threshold: 6,
preview_user_over_limit_free_plan_alert: 7 # EE-only
}
validates :group, presence: true
......
- add_page_specific_style 'page_bundles/members'
- page_title _('Group members')
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @group
.row.gl-mt-3
.col-lg-12
.gl-display-flex.gl-flex-wrap
......
......@@ -7,6 +7,7 @@
= render_if_exists 'shared/thanks_for_purchase_banner', plan_title: plan_title, quantity: params[:purchased_quantity].to_i
= render_if_exists 'shared/qrtly_reconciliation_alert', group: @group
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @group
- if show_invite_banner?(@group)
= content_for :group_invite_members_banner do
......
......@@ -20,6 +20,7 @@
= dispensable_render_if_exists "shared/namespace_user_cap_reached_alert"
= dispensable_render_if_exists "shared/new_user_signups_cap_reached_alert"
= yield :page_level_alert
= yield :user_over_limit_free_plan_alert
= yield :group_invite_members_banner
- unless @hide_breadcrumbs
= render "layouts/nav/breadcrumbs"
......
......@@ -3,6 +3,7 @@
- escaped_default_branch_name = default_branch_name.shellescape
- @skip_current_level_breadcrumb = true
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @project
= render partial: 'flash_messages', locals: { project: @project }
= render "home_panel"
......
- page_title _('No repository')
- @skip_current_level_breadcrumb = true
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @project
%h2.gl-display-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
= sprite_icon('warning-solid', size: 24, css_class: 'gl-mr-2')
......
- add_page_specific_style 'page_bundles/members'
- page_title _("Members")
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @project
.row.gl-mt-3
.col-lg-12
- if can_invite_members_for_project?(@project)
......
......@@ -6,6 +6,7 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity")
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @project
= render partial: 'flash_messages', locals: { project: @project }
= render "projects/last_push"
......
# frozen_string_literal: true
module EE
module Users
module GroupCalloutsHelper
extend ::Gitlab::Utils::Override
PREVIEW_USER_OVER_LIMIT_FREE_PLAN_ALERT = 'preview_user_over_limit_free_plan_alert'
def show_user_over_limit_free_plan_alert?(namespace)
return false if namespace.user_namespace?
return false if user_dismissed_for_group(PREVIEW_USER_OVER_LIMIT_FREE_PLAN_ALERT, namespace, 14.days.ago)
return false unless Ability.allowed?(current_user, :owner_access, namespace)
namespace.preview_free_user_cap_over?
end
end
end
end
......@@ -472,10 +472,16 @@ module EE
def free_user_cap_reached?
return false unless apply_free_user_cap?
free_plan_at_user_limit?
end
def preview_free_user_cap_over?
return false unless apply_preview_free_user_cap?
members_count = root_ancestor.free_plan_members_count
return false unless members_count
::Plan::FREE_USER_LIMIT <= members_count
members_count > ::Plan::FREE_USER_LIMIT
end
def user_limit_reached?(use_cache: false)
......@@ -490,6 +496,20 @@ module EE
private
def apply_preview_free_user_cap?
return false unless ::Gitlab::CurrentSettings.should_check_namespace_plan?
return false unless ::Feature.enabled?(:preview_free_user_cap, root_ancestor, default_enabled: :yaml)
has_free_or_no_subscription?
end
def free_plan_at_user_limit?
members_count = root_ancestor.free_plan_members_count
return false unless members_count
::Plan::FREE_USER_LIMIT <= members_count
end
# Members belonging directly to Projects within user/project namespaces
def billed_users
# this will include the namespace owner(user namespace) as well
......
- return unless show_user_over_limit_free_plan_alert?(source.root_ancestor)
- content_for :user_over_limit_free_plan_alert do
.container-fluid.container-limited{ class: "gl-pb-2! gl-pt-6! #{@content_class}" }
= render 'shared/global_alert',
alert_class: 'js-user-over-limit-free-plan-alert',
alert_data: { track_action: 'render',
track_label: 'user_limit_banner',
dismiss_endpoint: group_callouts_path,
feature_id: Users::GroupCalloutsHelper::PREVIEW_USER_OVER_LIMIT_FREE_PLAN_ALERT,
group_id: source.root_ancestor.id,
testid: 'user-over-limit-free-plan-alert' },
title: _('From June 22, 2022 (GitLab 15.1), free personal namespaces and top-level groups will be limited to %{free_limit} members') % { free_limit: ::Plan::FREE_USER_LIMIT },
close_button_data: { track_action: 'dismiss_banner',
track_label: 'user_limit_banner',
testid: 'user-over-limit-free-plan-dismiss' } do
.gl-alert-body
= _('Your %{doc_link_start}namespace%{doc_link_end}, %{strong_start}%{namespace_name}%{strong_end} has more than %{free_limit} members. From June 22, 2022, it will be limited to %{free_limit}, and the remaining members will get a %{link_start}status of Over limit%{link_end} and lose access to the namespace. You can go to the Usage Quotas page to manage which %{free_limit} members will remain in your namespace. To get more members, an owner can start a trial or upgrade to a paid tier.').html_safe % { namespace_name: source.root_ancestor.name,
free_limit: ::Plan::FREE_USER_LIMIT,
doc_link_start: '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/index', anchor: 'namespaces') },
doc_link_end: '</a>'.html_safe,
strong_start: "<strong>".html_safe,
strong_end: "</strong>".html_safe,
link_start: '<a href="https://about.gitlab.com/blog/2022/03/24/efficient-free-tier" target="_blank" rel="noopener noreferrer">'.html_safe,
link_end: '</a>'.html_safe }
.gl-alert-actions
= link_to _('Manage members'),
group_usage_quotas_path(source.root_ancestor),
class: 'btn gl-alert-action btn-info btn-md gl-button',
data: { track_action: 'click_button',
track_label: 'manage_members',
testid: 'user-over-limit-free-plan-manage' }
= link_to _('Explore paid plans'),
group_billings_path(source.root_ancestor),
class: 'btn gl-alert-action btn-default btn-md gl-button',
data: { track_action: 'click_button',
track_label: 'explore_paid_plans',
testid: 'user-over-limit-free-plan-explore' }
---
name: preview_free_user_cap
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83146
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/356561
milestone: '14.10'
type: development
group: group::conversion
default_enabled: false
......@@ -3,9 +3,8 @@
require 'spec_helper'
RSpec.describe 'Group information', :js, :aggregate_failures do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:empty_project) { create(:project, namespace: group) }
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
subject(:visit_page) { visit group_path(group) }
......@@ -49,7 +48,7 @@ RSpec.describe 'Group information', :js, :aggregate_failures do
end
end
describe 'qrtly reconciliation alert', :js do
describe 'qrtly reconciliation alert' do
context 'on self-managed' do
before do
visit_page
......@@ -82,4 +81,10 @@ RSpec.describe 'Group information', :js, :aggregate_failures do
end
end
end
context 'when over free user limit', :saas do
let_it_be(:group) { create(:group_with_plan, plan: :free_plan) }
it_behaves_like 'over the free user limit alert'
end
end
......@@ -80,4 +80,18 @@ RSpec.describe 'Groups > Members > List members' do
end
end
end
context 'when over free user limit', :saas do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group_with_plan, plan: :free_plan) }
subject(:visit_page) { visit group_group_members_path(group) }
before do
group.add_owner(user)
sign_in(user)
end
it_behaves_like 'over the free user limit alert'
end
end
......@@ -3,7 +3,6 @@
require 'spec_helper'
RSpec.describe 'Project > Members > Invite group and members' do
include Select2Helper
include ActionView::Helpers::DateHelper
include Spec::Support::Helpers::Features::MembersHelpers
......@@ -184,4 +183,19 @@ RSpec.describe 'Project > Members > Invite group and members' do
end
end
end
context 'when over free user limit', :saas do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group_with_plan, plan: :free_plan) }
let_it_be(:project) { create(:project, group: group) }
subject(:visit_page) { visit project_project_members_path(project) }
before do
group.add_owner(user)
sign_in(user)
end
it_behaves_like 'over the free user limit alert'
end
end
......@@ -62,4 +62,33 @@ RSpec.describe 'Project show page', :feature do
end
end
end
context 'when over free user limit', :saas do
let_it_be(:group) { create(:group_with_plan, plan: :free_plan) }
subject(:visit_page) { visit project_path(project) }
before do
group.add_owner(user)
sign_in(user)
end
context 'with repository' do
let_it_be(:project) { create(:project, :repository, group: group) }
it_behaves_like 'over the free user limit alert'
end
context 'with empty repository' do
let_it_be(:project) { create(:project, :empty_repo, group: group) }
it_behaves_like 'over the free user limit alert'
end
context 'without repository' do
let_it_be(:project) { create(:project, group: group) }
it_behaves_like 'over the free user limit alert'
end
end
end
# frozen_string_literal: true
require "spec_helper"
RSpec.describe EE::Users::GroupCalloutsHelper do
let_it_be(:user, refind: true) { create(:user) }
let_it_be(:group) { create(:group) }
before do
allow(helper).to receive(:current_user).and_return(user)
end
describe '.show_user_over_limit_free_plan_alert?' do
let(:preview_free_user_cap_over?) { true }
subject { helper.show_user_over_limit_free_plan_alert?(group) }
before do
allow(group).to receive(:preview_free_user_cap_over?).and_return(preview_free_user_cap_over?)
end
context 'when it is a group namespace' do
context 'when user has the owner_access ability for the group' do
before do
group.add_owner(user)
end
context 'when the invite_members_banner has not been dismissed' do
it { is_expected.to eq(true) }
context 'when preview_free_user_cap_over? is false' do
let(:preview_free_user_cap_over?) { false }
it { is_expected.to eq(false) }
end
end
context 'when the preview_user_over_limit_free_plan_alert has been dismissed' do
before do
create(:group_callout,
user: user,
group: group,
feature_name: described_class::PREVIEW_USER_OVER_LIMIT_FREE_PLAN_ALERT,
dismissed_at: Time.now)
end
it { is_expected.to eq(false) }
end
context 'when the preview_user_over_limit_free_plan_alert dismissal is no longer valid after 7 days' do
before do
create(:group_callout,
user: user,
group: group,
feature_name: described_class::PREVIEW_USER_OVER_LIMIT_FREE_PLAN_ALERT,
dismissed_at: 15.days.ago)
end
it { is_expected.to eq(true) }
end
end
context 'when user does not have owner_access ability for the group' do
it { is_expected.to eq(false) }
end
end
context 'when it is a user_namespace' do
let_it_be(:group) { user.namespace }
it { is_expected.to eq(false) }
end
end
end
......@@ -976,6 +976,86 @@ RSpec.describe Namespace do
end
end
describe '#preview_free_user_cap_over?', :saas do
let_it_be(:namespace) { create(:group_with_plan, plan: :free_plan) }
let(:should_check_namespace_plan) { true }
before do
stub_ee_application_setting(should_check_namespace_plan: should_check_namespace_plan)
end
subject(:preview_free_user_cap_over?) { namespace.preview_free_user_cap_over? }
context 'when :preview_free_user_cap is disabled' do
before do
stub_feature_flags(preview_free_user_cap: false)
end
it { is_expected.to be false }
end
context 'when :preview_free_user_cap is enabled' do
before do
stub_feature_flags(preview_free_user_cap: true)
end
it { is_expected.to be false }
context 'when the member counts should be compared for that root ancestor' do
before do
allow(namespace).to receive(:free_plan_members_count).and_return(free_plan_members_count)
end
context 'when under the number of free users limit' do
let(:free_plan_members_count) { 3 }
it { is_expected.to be false }
end
context 'when at the same number as the free users limit' do
let(:free_plan_members_count) { ::Plan::FREE_USER_LIMIT }
it { is_expected.to be false }
end
context 'when over the number of free users limit' do
let(:free_plan_members_count) { 6 }
it { is_expected.to be true }
context 'when the namespace is not a group' do
let_it_be(:namespace) do
namespace = create(:user).namespace
create(:gitlab_subscription, hosted_plan: create(:free_plan), namespace: namespace)
namespace
end
it { is_expected.to be true }
end
context 'when it is a non free plan' do
let_it_be(:namespace) { create(:group_with_plan, plan: :ultimate_plan) }
it { is_expected.to be false }
end
context 'when no plan exists' do
let_it_be(:namespace) { create(:group) }
it { is_expected.to be true }
end
context 'when should check namespace plan is false' do
let(:should_check_namespace_plan) { false }
it { is_expected.to be false }
end
end
end
end
end
describe '#user_limit_reached?' do
where(:free_user_cap_reached) do
[
......
# frozen_string_literal: true
RSpec.shared_examples_for 'over the free user limit alert' do
before do
stub_ee_application_setting(should_check_namespace_plan: true)
stub_feature_flags(preview_free_user_cap: true)
stub_const('::Plan::FREE_USER_LIMIT', 1)
end
it 'shows free user limit warning and honors dismissal', :js do
alert_title_content = 'From June 22, 2022 (GitLab 15.1), free personal namespaces and top-level groups will be limited to'
visit_page
expect(page).not_to have_content(alert_title_content)
group.add_developer(create(:user))
page.refresh
expect(page).to have_content(alert_title_content)
page.within('[data-testid="user-over-limit-free-plan-alert"]') do
expect(page).to have_link('Manage members')
expect(page).to have_link('Explore paid plans')
end
find('[data-testid="user-over-limit-free-plan-dismiss"]').click
visit visit_page
expect(page).not_to have_content(alert_title_content)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'groups/group_members/index' do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
before do
allow(view).to receive(:group_members_app_data).and_return({})
allow(view).to receive(:current_user).and_return(user)
assign(:group, group)
end
context 'when free plan limit alert is present' do
it 'renders the alert partial' do
render
expect(rendered).to render_template('shared/_user_over_limit_free_plan_alert')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'groups/show' do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
before do
allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:group_path).and_return('_group_path_')
allow(view).to receive(:group_shared_path).and_return('_group_shared_path_')
allow(view).to receive(:group_archived_path).and_return('_group_archived_path_')
assign(:group, group)
end
context 'when free plan limit alert is present' do
it 'renders the alert partial' do
render
expect(rendered).to render_template('shared/_user_over_limit_free_plan_alert')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'projects/empty' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { ProjectPresenter.new(create(:project, :empty_repo), current_user: user) }
let(:can_admin_project_member) { true }
before do
allow(view).to receive(:current_user).and_return(user)
assign(:project, project)
end
context 'when free plan limit alert is present' do
it 'renders the alert partial' do
render
expect(rendered).to render_template('shared/_user_over_limit_free_plan_alert')
end
end
end
......@@ -69,4 +69,12 @@ RSpec.describe 'projects/project_members/index', :aggregate_failures do
end
end
end
context 'when free plan limit alert is present' do
it 'renders the alert partial' do
render
expect(rendered).to render_template('shared/_user_over_limit_free_plan_alert')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'projects/show' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { ProjectPresenter.new(create(:project, :empty_repo), current_user: user) }
let(:can_admin_project_member) { true }
before do
allow(view).to receive(:current_user).and_return(user)
assign(:project, project)
stub_template 'projects/_activity.html.haml' => ''
end
context 'when free plan limit alert is present' do
it 'renders the alert partial' do
render
expect(rendered).to render_template('shared/_user_over_limit_free_plan_alert')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'shared/user_over_limit_free_plan_alert' do
let_it_be(:source) { create(:group) }
let(:partial) { 'shared/user_over_limit_free_plan_alert' }
before do
allow(view).to receive(:source).and_return(source)
allow(view).to receive(:show_user_over_limit_free_plan_alert?).with(source).and_return(true)
end
it 'renders all the expected tracking items', :aggregate_failures do
render partial
expect(view.content_for(:user_over_limit_free_plan_alert))
.to have_css('.js-user-over-limit-free-plan-alert[data-track-action="render"][data-track-label="user_limit_banner"]')
expect(view.content_for(:user_over_limit_free_plan_alert))
.to have_css('[data-testid="user-over-limit-free-plan-dismiss"][data-track-action="dismiss_banner"][data-track-label="user_limit_banner"]')
expect(view.content_for(:user_over_limit_free_plan_alert))
.to have_css('[data-testid="user-over-limit-free-plan-manage"][data-track-action="click_button"][data-track-label="manage_members"]')
expect(view.content_for(:user_over_limit_free_plan_alert))
.to have_css('[data-testid="user-over-limit-free-plan-explore"][data-track-action="click_button"][data-track-label="explore_paid_plans"]')
end
it 'renders all the correct links and buttons', :aggregate_failures do
render partial
expect(view.content_for(:user_over_limit_free_plan_alert))
.to have_link('Manage members', href: group_usage_quotas_path(source))
expect(view.content_for(:user_over_limit_free_plan_alert))
.to have_link('Explore paid plans', href: group_billings_path(source))
expect(view.content_for(:user_over_limit_free_plan_alert))
.to have_link('status of Over limit', href: 'https://about.gitlab.com/blog/2022/03/24/efficient-free-tier')
expect(view.content_for(:user_over_limit_free_plan_alert))
.to have_css("[data-testid='user-over-limit-free-plan-alert'][data-dismiss-endpoint='#{group_callouts_path}'][data-feature-id='#{Users::GroupCalloutsHelper::PREVIEW_USER_OVER_LIMIT_FREE_PLAN_ALERT}'][data-group-id='#{source.id}']")
end
end
......@@ -15114,6 +15114,9 @@ msgstr ""
msgid "Explore groups"
msgstr ""
msgid "Explore paid plans"
msgstr ""
msgid "Explore projects"
msgstr ""
......@@ -16219,6 +16222,9 @@ msgstr ""
msgid "From %{providerTitle}"
msgstr ""
msgid "From June 22, 2022 (GitLab 15.1), free personal namespaces and top-level groups will be limited to %{free_limit} members"
msgstr ""
msgid "From issue creation until deploy to production"
msgstr ""
......@@ -22787,6 +22793,9 @@ msgstr ""
msgid "Manage labels"
msgstr ""
msgid "Manage members"
msgstr ""
msgid "Manage milestones"
msgstr ""
......@@ -43128,6 +43137,9 @@ msgstr ""
msgid "YouTube URL or ID"
msgstr ""
msgid "Your %{doc_link_start}namespace%{doc_link_end}, %{strong_start}%{namespace_name}%{strong_end} has more than %{free_limit} members. From June 22, 2022, it will be limited to %{free_limit}, and the remaining members will get a %{link_start}status of Over limit%{link_end} and lose access to the namespace. You can go to the Usage Quotas page to manage which %{free_limit} members will remain in your namespace. To get more members, an owner can start a trial or upgrade to a paid tier."
msgstr ""
msgid "Your %{group} membership will now expire in %{days}."
msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'groups/edit.html.haml' do
include Devise::Test::ControllerHelpers
describe '"Share with group lock" setting' do
let(:root_owner) { create(:user) }
let(:root_group) { create(:group) }
before do
root_group.add_owner(root_owner)
end
shared_examples_for '"Share with group lock" setting' do |checkbox_options|
it 'has the correct label, help text, and checkbox options' do
assign(:group, test_group)
allow(view).to receive(:can?).with(test_user, :admin_group, test_group).and_return(true)
allow(view).to receive(:can_change_group_visibility_level?).and_return(false)
allow(view).to receive(:current_user).and_return(test_user)
expect(view).to receive(:can_change_share_with_group_lock?).and_return(!checkbox_options[:disabled])
expect(view).to receive(:share_with_group_lock_help_text).and_return('help text here')
render
expect(rendered).to have_content("Prevent sharing a project within #{test_group.name} with other groups")
expect(rendered).to have_content('help text here')
expect(rendered).to have_field('group_share_with_group_lock', **checkbox_options)
end
end
context 'for a root group' do
let(:test_group) { root_group }
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
context 'for a subgroup' do
let!(:subgroup) { create(:group, parent: root_group) }
let(:sub_owner) { create(:user) }
let(:test_group) { subgroup }
context 'when the root_group has "Share with group lock" disabled' do
context 'when the subgroup has "Share with group lock" disabled' do
context 'as the root_owner' do
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
context 'as the sub_owner' do
let(:test_user) { sub_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
end
context 'when the subgroup has "Share with group lock" enabled' do
before do
subgroup.update_column(:share_with_group_lock, true)
end
context 'as the root_owner' do
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
end
context 'as the sub_owner' do
let(:test_user) { sub_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
end
end
end
context 'when the root_group has "Share with group lock" enabled' do
before do
root_group.update_column(:share_with_group_lock, true)
end
context 'when the subgroup has "Share with group lock" disabled (parent overridden)' do
context 'as the root_owner' do
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
context 'as the sub_owner' do
let(:test_user) { sub_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
end
context 'when the subgroup has "Share with group lock" enabled (same as parent)' do
before do
subgroup.update_column(:share_with_group_lock, true)
end
context 'as the root_owner' do
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
end
context 'as the sub_owner' do
let(:test_user) { sub_owner }
it_behaves_like '"Share with group lock" setting', { disabled: true, checked: true }
end
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment