Skip to content
Snippets Groups Projects
Verified Commit 78398c40 authored by Robert May's avatar Robert May Committed by GitLab
Browse files

Merge branch 'vij-add-invite-modal-alert' into 'master'

Add an alert on blocked seat overages

See merge request !147896



Merged-by: default avatarRobert May <rmay@gitlab.com>
Approved-by: default avatarAngelo Gulina <agulina@gitlab.com>
Approved-by: default avatarRobert May <rmay@gitlab.com>
Reviewed-by: default avatarRobert May <rmay@gitlab.com>
Reviewed-by: default avatarVijay Hawoldar <vhawoldar@gitlab.com>
Reviewed-by: default avatarAngelo Gulina <agulina@gitlab.com>
Co-authored-by: default avatarVijay Hawoldar <vhawoldar@gitlab.com>
parents 992fa930 eccca2a4
No related branches found
No related tags found
1 merge request!147896Add an alert on blocked seat overages
Pipeline #1250714521 passed
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
......
......@@ -27710,6 +27710,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 ""
 
......@@ -27751,6 +27754,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