Skip to content
Snippets Groups Projects
Verified Commit eccca2a4 authored by Vijay Hawoldar's avatar Vijay Hawoldar Committed by GitLab
Browse files

Add an alert on blocked seat overages

When inviting members to a group or project, render an alert if the
invite is blocked due to seat overages
parent 6dd81499
No related branches found
No related tags found
1 merge request!147896Add an alert on blocked seat overages
Showing
with 115 additions and 8 deletions
......@@ -10,6 +10,9 @@ import { n__, sprintf } from '~/locale';
import { memberName, triggerExternalAlert } from 'ee_else_ce/invite_members/utils/member_utils';
import { captureException } from '~/ci/runner/sentry_utils';
import {
BLOCKED_SEAT_OVERAGES_ERROR_REASON,
BLOCKED_SEAT_OVERAGES_BODY,
BLOCKED_SEAT_OVERAGES_CTA,
USERS_FILTER_ALL,
MEMBER_MODAL_LABELS,
INVITE_MEMBER_MODAL_TRACKING_CATEGORY,
......@@ -43,6 +46,11 @@ export default {
SafeHtml,
},
mixins: [Tracking.mixin({ category: INVITE_MEMBER_MODAL_TRACKING_CATEGORY })],
inject: {
addSeatsHref: {
default: '',
},
},
props: {
id: {
type: String,
......@@ -109,6 +117,7 @@ export default {
},
data() {
return {
errorReason: '',
invalidFeedbackMessage: '',
isLoading: false,
modalId: uniqueId('invite-members-modal-'),
......@@ -182,6 +191,9 @@ export default {
formGroupDescription() {
return this.invalidFeedbackMessage ? null : this.$options.labels.placeHolder;
},
shouldShowSeatOverageNotification() {
return this.errorReason === BLOCKED_SEAT_OVERAGES_ERROR_REASON;
},
},
watch: {
isEmptyInvites: {
......@@ -270,6 +282,7 @@ export default {
const { error, message } = responseFromSuccess(response);
if (error) {
this.errorReason = response.data.reason;
this.showErrors(message);
} else {
this.onInviteSuccess();
......@@ -322,6 +335,7 @@ export default {
this.closeModal();
},
clearValidation() {
this.errorReason = '';
this.invalidFeedbackMessage = '';
this.invalidMembers = {};
},
......@@ -338,6 +352,10 @@ export default {
},
},
labels: MEMBER_MODAL_LABELS,
i18n: {
BLOCKED_SEAT_OVERAGES_BODY,
BLOCKED_SEAT_OVERAGES_CTA,
},
};
</script>
<template>
......@@ -462,5 +480,20 @@ export default {
@token-remove="removeToken"
/>
</template>
<template #after-members-input>
<gl-alert
v-if="shouldShowSeatOverageNotification"
id="seat-overages-alert"
class="gl-mb-4"
dismissable
data-testid="seat-overages-alert"
:primary-button-link="addSeatsHref"
:primary-button-text="$options.i18n.BLOCKED_SEAT_OVERAGES_CTA"
@dismiss="errorReason = false"
>
{{ $options.i18n.BLOCKED_SEAT_OVERAGES_BODY }}
</gl-alert>
</template>
</invite-modal-base>
</template>
......@@ -315,6 +315,8 @@ export default {
<slot name="select" v-bind="{ exceptionState, inputId: selectId }"></slot>
</gl-form-group>
<slot name="after-members-input"></slot>
<gl-form-group
class="gl-sm-w-half gl-w-full"
:label="$options.ACCESS_LEVEL"
......
......@@ -159,3 +159,8 @@ export const REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE = REACHED_LIMIT_MESSAGE.co
export const CLOSE_TO_LIMIT_MESSAGE = s__(
'InviteMembersModal|To get more members an owner of the group can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier.',
);
export const BLOCKED_SEAT_OVERAGES_BODY = s__(
'InviteMembersModal|You must purchase more seats for your subscription before this amount of users can be added.',
);
export const BLOCKED_SEAT_OVERAGES_CTA = s__('InviteMembersModal|Purchase more seats');
export const BLOCKED_SEAT_OVERAGES_ERROR_REASON = 'seat_limit_exceeded_error';
......@@ -27,6 +27,7 @@ export default (function initInviteMembersModal() {
name: el.dataset.name,
overageMembersModalAvailable: parseBoolean(el.dataset.overageMembersModalAvailable),
hasGitlabSubscription: parseBoolean(el.dataset.hasGitlabSubscription),
addSeatsHref: el.dataset.addSeatsHref,
},
render: (createElement) =>
createElement(InviteMembersModal, {
......
......@@ -40,7 +40,8 @@ def execute
result
rescue BlankInvitesError, TooManyInvitesError, MembershipLockedError, SeatLimitExceededError => e
Gitlab::ErrorTracking.log_exception(e, class: self.class.to_s, user_id: current_user.id)
error(e.message)
error(e.message, pass_back: { reason: e.class.name.demodulize.underscore.to_sym })
end
def single_member
......
......@@ -34,6 +34,7 @@ def common_invite_modal_dataset(source)
dataset[:manage_member_roles_path] = manage_member_roles_path(source)
dataset[:overage_members_modal_available] = overage_members_modal_available.to_s
dataset[:has_gitlab_subscription] = gitlab_com_subscription?.to_s
dataset[:add_seats_href] = add_seats_url(source.root_ancestor)
dataset
end
......
......@@ -27,6 +27,11 @@
stub_ee_application_setting(dashboard_limit_enabled: true)
end
it 'includes add_seats_href' do
expect(helper.common_invite_modal_dataset(project)[:add_seats_href])
.to eq(::Gitlab::Routing.url_helpers.subscription_portal_add_extra_seats_url(project.root_ancestor.id))
end
context 'when applying the free user cap is not valid' do
let!(:group) do
create(:group_with_plan, :private, projects: [project], plan: :default_plan)
......
......@@ -122,7 +122,8 @@
expect(group.members.map(&:user_id)).to contain_exactly(owner.id)
expect(json_response).to eq({
'status' => 'error',
'message' => 'There are not enough available seats to invite this many users.'
'message' => 'There are not enough available seats to invite this many users.',
'reason' => 'seat_limit_exceeded_error'
})
end
......@@ -134,7 +135,8 @@
expect(group.members.map(&:user_id)).to contain_exactly(owner.id)
expect(json_response).to eq({
'status' => 'error',
'message' => 'There are not enough available seats to invite this many users.'
'message' => 'There are not enough available seats to invite this many users.',
'reason' => 'seat_limit_exceeded_error'
})
end
......@@ -337,6 +339,7 @@
expect(json_response['message']).to eq 'Members::CreateService::MembershipLockedError'
expect(json_response['status']).to eq 'error'
expect(json_response['reason']).to eq 'membership_locked_error'
end
end
......@@ -383,7 +386,8 @@
expect(project.members.map(&:user_id)).to be_empty
expect(json_response).to eq({
'status' => 'error',
'message' => 'There are not enough available seats to invite this many users.'
'message' => 'There are not enough available seats to invite this many users.',
'reason' => 'seat_limit_exceeded_error'
})
end
end
......@@ -401,7 +405,8 @@
expect(project.members.map(&:user_id)).to contain_exactly(maintainer.id)
expect(json_response).to eq({
'status' => 'error',
'message' => 'There are not enough available seats to invite this many users. Ask a user with the Owner role to purchase more seats.'
'message' => 'There are not enough available seats to invite this many users. Ask a user with the Owner role to purchase more seats.',
'reason' => 'seat_limit_exceeded_error'
})
end
......@@ -421,7 +426,8 @@
expect(project.members.map(&:user_id)).to contain_exactly(maintainer.id)
expect(json_response).to eq({
'status' => 'error',
'message' => 'There are not enough available seats to invite this many users. Ask a user with the Owner role to purchase more seats.'
'message' => 'There are not enough available seats to invite this many users. Ask a user with the Owner role to purchase more seats.',
'reason' => 'seat_limit_exceeded_error'
})
end
end
......
......@@ -631,7 +631,8 @@
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq({
'message' => 'There are not enough available seats to invite this many users.',
'status' => 'error'
'status' => 'error',
'reason' => 'seat_limit_exceeded_error'
})
end
end
......
......@@ -90,7 +90,8 @@
expect(result).to eq({
status: :error,
message: 'There are not enough available seats to invite this many users. ' \
'Ask a user with the Owner role to purchase more seats.'
'Ask a user with the Owner role to purchase more seats.',
reason: :seat_limit_exceeded_error
})
end
end
......
......@@ -27704,6 +27704,9 @@ msgstr ""
msgid "InviteMembersModal|Please add members to invite"
msgstr ""
 
msgid "InviteMembersModal|Purchase more seats"
msgstr ""
msgid "InviteMembersModal|Review the invite errors and try again:"
msgstr ""
 
......@@ -27745,6 +27748,9 @@ msgstr ""
msgid "InviteMembersModal|Username, name or email address"
msgstr ""
 
msgid "InviteMembersModal|You must purchase more seats for your subscription before this amount of users can be added."
msgstr ""
msgid "InviteMembersModal|You only have space for %{count} more %{members} in %{name}"
msgstr ""
 
......@@ -133,6 +133,7 @@ describe('InviteMembersModal', () => {
const findUserLimitAlert = () => wrapper.findComponent(UserLimitNotification);
const findAccordion = () => wrapper.findComponent(GlCollapse);
const findErrorsIcon = () => wrapper.findComponent(GlIcon);
const findSeatOveragesAlert = () => wrapper.findByTestId('seat-overages-alert');
const expectedErrorMessage = (index, errorType) => {
const [username, message] = Object.entries(errorType.parsedMessage)[index];
return `${username}: ${message}`;
......@@ -830,5 +831,17 @@ describe('InviteMembersModal', () => {
});
});
});
describe('blocked seat overage error notifications', () => {
it('shows the notification alert when seat overage limit is reached', async () => {
createInviteMembersToGroupWrapper();
await triggerMembersTokenSelect([user1]);
mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.ERROR_SEAT_LIMIT_REACHED);
clickInviteButton();
await waitForPromises();
expect(findSeatOveragesAlert().exists()).toBe(true);
});
});
});
});
......@@ -72,6 +72,12 @@ const INVITE_LIMIT = {
status: 'error',
};
const ERROR_SEAT_LIMIT_REACHED = {
message: 'No seats available',
status: 'error',
reason: 'seat_limit_exceeded_error',
};
export const GROUPS_INVITATIONS_PATH = '/api/v4/groups/1/invitations';
export const invitationsApiResponse = {
......@@ -82,6 +88,7 @@ export const invitationsApiResponse = {
EMAIL_TAKEN,
EXPANDED_RESTRICTED,
INVITE_LIMIT,
ERROR_SEAT_LIMIT_REACHED,
};
export const IMPORT_PROJECT_MEMBERS_PATH = '/api/v4/projects/1/import_project_members/2';
......
......@@ -207,6 +207,7 @@
expect(execute_service[:status]).to eq(:error)
expect(execute_service[:message]).to eq(s_('AddMember|No users specified.'))
expect(execute_service[:reason]).to eq(:blank_invites_error)
expect(source.users).not_to include member
expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(false)
end
......@@ -222,6 +223,7 @@
expect(execute_service[:status]).to eq(:error)
expect(execute_service[:message]).to be_present
expect(execute_service[:reason]).to eq(:too_many_invites_error)
expect(source.users).not_to include member
expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(false)
end
......@@ -336,4 +338,27 @@
end
end
end
context 'with raised errors' do
using RSpec::Parameterized::TableSyntax
where(:error, :stubbed_method, :reason) do
described_class::BlankInvitesError | :validate_invite_source! | :blank_invites_error
described_class::TooManyInvitesError | :validate_invitable! | :too_many_invites_error
described_class::MembershipLockedError | :add_members | :membership_locked_error
described_class::SeatLimitExceededError | :add_members | :seat_limit_exceeded_error
end
with_them do
before do
allow_next_instance_of(described_class) do |service|
allow(service).to receive(stubbed_method).and_raise(error)
end
end
it 'returns the correct reason' do
expect(execute_service[:reason]).to eq(reason)
end
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment