Commit 775c519e authored by Kerri Miller's avatar Kerri Miller

Merge branch 'ag/348481-saas-seats-count-alert-callout' into 'master'

Add Groups Callout functionality for Alert

See merge request gitlab-org/gitlab!78260
parents 18862ca7 b6279f98
......@@ -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