Commit b6279f98 authored by Angelo Gulina's avatar Angelo Gulina Committed by Kerri Miller

Permanent dismissal for the alert banner

When the Sears Count Alert is dismissed, the choice will be
remembered untill the next time seats are updated
parent b6a1aaa8
......@@ -7,10 +7,11 @@ const DEFERRED_LINK_CLASS = 'deferred-link';
export default class PersistentUserCallout {
constructor(container, options = container.dataset) {
const { dismissEndpoint, featureId, deferLinks } = options;
const { dismissEndpoint, featureId, groupId, deferLinks } = options;
this.container = container;
this.dismissEndpoint = dismissEndpoint;
this.featureId = featureId;
this.groupId = groupId;
this.deferLinks = parseBoolean(deferLinks);
this.init();
......@@ -52,6 +53,7 @@ export default class PersistentUserCallout {
axios
.post(this.dismissEndpoint, {
feature_name: this.featureId,
group_id: this.groupId,
})
.then(() => {
this.container.remove();
......
......@@ -10,6 +10,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-new-user-signups-cap-reached',
'.js-eoa-bronze-plan-banner',
'.js-security-newsletter-callout',
'.js-approaching-seats-count-threshold',
];
const initCallouts = () => {
......
......@@ -3,6 +3,7 @@
module Users
module GroupCalloutsHelper
INVITE_MEMBERS_BANNER = 'invite_members_banner'
APPROACHING_SEAT_COUNT_THRESHOLD = 'approaching_seat_count_threshold'
def show_invite_banner?(group)
Ability.allowed?(current_user, :admin_group, group) &&
......
......@@ -9,7 +9,8 @@ module Users
belongs_to :group
enum feature_name: {
invite_members_banner: 1
invite_members_banner: 1,
approaching_seat_count_threshold: 2 # EE-only
}
validates :group, presence: true
......
......@@ -26,9 +26,8 @@ module SeatsCountAlertHelper
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
return false unless ::Gitlab.dev_env_or_com? && group_with_owner? && current_subscription
return false if user_dismissed_alert?
!!@display_seats_count_alert
end
......@@ -39,6 +38,22 @@ module SeatsCountAlertHelper
private
def user_dismissed_alert?
current_user.dismissed_callout_for_group?(
feature_name: Users::GroupCalloutsHelper::APPROACHING_SEAT_COUNT_THRESHOLD,
group: root_namespace,
ignore_dismissal_earlier_than: last_member_added_at
)
end
def last_member_added_at
root_namespace&.last_billed_user_created_at
end
def group_with_owner?
root_namespace&.group_namespace? && root_namespace&.has_owner?(current_user)
end
def root_namespace
@project&.root_ancestor || @group&.root_ancestor
end
......
......@@ -462,6 +462,10 @@ module EE
levels.merge(::Gitlab::Access::MINIMAL_ACCESS_HASH)
end
def last_billed_user_created_at
billed_group_and_projects_members.reverse_order.limit(1).pluck(:created_at).first
end
override :users_count
def users_count
return all_group_members.count if minimal_access_role_allowed?
......@@ -610,6 +614,15 @@ module EE
end
end
def billed_group_and_projects_members
::Member
.in_hierarchy(self)
.active
.non_guests
.non_invite
.order(:created_at)
end
# Members belonging directly to Group or its subgroups
def billed_group_users(non_guests: false)
members = ::GroupMember.active_without_invites_and_requests.where(
......
- return unless show_seats_count_alert?
.container.container-limited.pt-3
.gl-alert.gl-alert-info{ role: 'alert' }
.gl-alert.gl-alert-info.js-approaching-seats-count-threshold{ role: 'alert', data: { dismiss_endpoint: group_callouts_path,
feature_id: Users::GroupCalloutsHelper::APPROACHING_SEAT_COUNT_THRESHOLD,
group_id: root_namespace.id } }
= sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon')
%button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
%button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss'), data: { testid: 'approaching-seats-count-threshold-alert-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 }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Display approaching seats count threshold alert', :saas, :js do
let_it_be(:user) { create(:user) }
shared_examples_for 'a hidden alert' do
it 'does not show the alert' do
visit visit_path
expect(page).not_to have_content("#{group.name} is approaching the limit of available seats")
expect(page).not_to have_link('View seat usage', href: usage_quotas_path(group, anchor: 'seats-quota-tab'))
end
end
shared_examples_for 'a visible alert' do
it 'shows the alert' do
visit visit_path
expect(page).to have_content("#{group.name} is approaching the limit of available seats")
expect(page).to have_content("Your subscription has #{gitlab_subscription.seats - gitlab_subscription.seats_in_use} out of #{gitlab_subscription.seats} 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.")
expect(page).to have_link('View seat usage', href: usage_quotas_path(group, anchor: 'seats-quota-tab'))
end
end
shared_examples_for 'a dismissed alert' do
context 'when alert was dismissed' do
before do
visit visit_path
find('body.page-initialised [data-testid="approaching-seats-count-threshold-alert-dismiss"]').click
end
it_behaves_like 'a hidden alert'
end
end
context 'when conditions not met' do
let_it_be(:group) { create(:group) }
let_it_be(:visit_path) { group_path(group) }
context 'when logged out' do
it_behaves_like 'a hidden alert'
end
context 'when logged in owner' do
before do
group.add_owner(user)
sign_in(user)
end
it_behaves_like 'a hidden alert'
end
end
end
......@@ -823,6 +823,39 @@ RSpec.describe Group do
end
end
describe '#last_billed_user_created_at' do
subject(:last_billed) { group.last_billed_user_created_at }
let(:group) { create(:group) }
let(:user) { create(:user) }
context 'without billed users' do
it { is_expected.to be nil }
end
context 'with guest users' do
before do
create(:group_member, :guest, user: user, source: group)
end
it { is_expected.to be nil }
end
context 'with billed users' do
let_it_be(:expected_time) { Time.new(2022, 4, 19, 00, 00, 00, '+00:00') }
before do
create(:group_member, user: create(:user), source: group, created_at: expected_time)
create(:group_member, :guest, user: user, source: group, created_at: '2022-07-02')
create(:group_member, user: create(:user), source: group, created_at: '2022-03-16')
end
it 'returns the last added billed member' do
expect(last_billed).to be_like_time(expected_time)
end
end
end
describe '#saml_discovery_token' do
it 'returns existing tokens' do
group = create(:group, saml_discovery_token: 'existing')
......
......@@ -10,6 +10,7 @@ jest.mock('~/flash');
describe('PersistentUserCallout', () => {
const dismissEndpoint = '/dismiss';
const featureName = 'feature';
const groupId = '5';
function createFixture() {
const fixture = document.createElement('div');
......@@ -18,6 +19,7 @@ describe('PersistentUserCallout', () => {
class="container"
data-dismiss-endpoint="${dismissEndpoint}"
data-feature-id="${featureName}"
data-group-id="${groupId}"
>
<button type="button" class="js-close"></button>
</div>
......@@ -86,7 +88,9 @@ describe('PersistentUserCallout', () => {
return waitForPromises().then(() => {
expect(persistentUserCallout.container.remove).toHaveBeenCalled();
expect(mockAxios.history.post[0].data).toBe(JSON.stringify({ feature_name: featureName }));
expect(mockAxios.history.post[0].data).toBe(
JSON.stringify({ feature_name: featureName, group_id: groupId }),
);
});
});
......@@ -191,8 +195,8 @@ describe('PersistentUserCallout', () => {
return waitForPromises().then(() => {
expect(window.location.assign).toBeCalledWith(href);
expect(mockAxios.history.post[0].data).toBe(JSON.stringify({ feature_name: featureName }));
expect(persistentUserCallout.container.remove).not.toHaveBeenCalled();
expect(mockAxios.history.post[0].data).toBe(JSON.stringify({ feature_name: featureName }));
});
});
......
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