diff --git a/config/feature_flags/development/limited_access_modal.yml b/config/feature_flags/development/limited_access_modal.yml new file mode 100644 index 0000000000000000000000000000000000000000..b567b9ce0d4fa91c1bb7dc3fb01e61a95a053392 --- /dev/null +++ b/config/feature_flags/development/limited_access_modal.yml @@ -0,0 +1,8 @@ +--- +name: limited_access_modal +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129790 +rollout_issue_url: +milestone: '16.4' +type: development +group: group::billing and subscription management +default_enabled: false diff --git a/ee/app/assets/javascripts/usage_quotas/components/limited_access_modal.vue b/ee/app/assets/javascripts/usage_quotas/components/limited_access_modal.vue new file mode 100644 index 0000000000000000000000000000000000000000..136d818f346e6e97a4789a17e9b4869c53ec382d --- /dev/null +++ b/ee/app/assets/javascripts/usage_quotas/components/limited_access_modal.vue @@ -0,0 +1,62 @@ +<script> +import { GlModal } from '@gitlab/ui'; +import { __, s__ } from '~/locale'; + +const LIMITED_ACCESS_MESSAGING = Object.freeze({ + MANAGED_BY_RESELLER: { + title: s__('SubscriptionMangement|Your subscription is in read-only mode'), + content: s__( + 'SubscriptionMangement|To make changes to a read-only subscription or purchase additional products, contact your GitLab Partner.', + ), + }, + RAMP_SUBSCRIPTION: { + title: s__( + 'SubscriptionMangement|This is a custom subscription managed by the GitLab Sales team', + ), + content: s__( + "SubscriptionMangement|If you'd like to add more seats, upgrade your plan, or purchase additional products, contact your GitLab sales representative.", + ), + }, +}); + +export default { + name: 'LimitedAccessModal', + components: { GlModal }, + props: { + limitedAccessReason: { + type: String, + // defaults to 'MANAGED_BY_RESELLER' till we have API wired + default: 'MANAGED_BY_RESELLER', + validator: (prop) => ['MANAGED_BY_RESELLER', 'RAMP_SUBSCRIPTION'].includes(prop), + required: false, + }, + }, + computed: { + limitedAccessData() { + return LIMITED_ACCESS_MESSAGING[this.limitedAccessReason]; + }, + modalTitle() { + return this.limitedAccessData.title; + }, + modalContent() { + return this.limitedAccessData.content; + }, + primaryAction() { + return { + text: __('Close'), + attributes: { variant: 'confirm' }, + }; + }, + }, +}; +</script> +<template> + <gl-modal + :action-primary="primaryAction" + modal-id="limited-access-modal-id" + :title="modalTitle" + data-testid="limited-access-modal-id" + > + {{ modalContent }} + </gl-modal> +</template> diff --git a/ee/app/assets/javascripts/usage_quotas/seats/components/statistics_seats_card.vue b/ee/app/assets/javascripts/usage_quotas/seats/components/statistics_seats_card.vue index e755f1aa538aceac79c6c7dc4034180a3735cbb0..d313b943dd6ad27765b6f4e036c905599d5c23e5 100644 --- a/ee/app/assets/javascripts/usage_quotas/seats/components/statistics_seats_card.vue +++ b/ee/app/assets/javascripts/usage_quotas/seats/components/statistics_seats_card.vue @@ -1,5 +1,5 @@ <script> -import { GlLink, GlIcon, GlButton } from '@gitlab/ui'; +import { GlLink, GlIcon, GlButton, GlModalDirective } from '@gitlab/ui'; import { addSeatsText, seatsOwedHelpText, @@ -10,10 +10,15 @@ import { seatsUsedText, } from 'ee/usage_quotas/seats/constants'; import Tracking from '~/tracking'; +import { visitUrl } from '~/lib/utils/url_utility'; +import LimitedAccessModal from '../../components/limited_access_modal.vue'; export default { name: 'StatisticsSeatsCard', - components: { GlLink, GlIcon, GlButton }, + components: { GlLink, GlIcon, GlButton, LimitedAccessModal }, + directives: { + GlModalDirective, + }, helpLinks: { seatsUsedLink, seatsOwedLink, @@ -60,6 +65,11 @@ export default { default: null, }, }, + data() { + return { + showLimitedAccessModal: false, + }; + }, computed: { shouldRenderSeatsUsedBlock() { return this.seatsUsed !== null; @@ -67,11 +77,23 @@ export default { shouldRenderSeatsOwedBlock() { return this.seatsOwed !== null; }, + shouldShowModal() { + return gon.features?.limitedAccessModal; + }, }, methods: { trackClick() { this.track('click_button', { label: 'add_seats_saas', property: 'usage_quotas_page' }); }, + handleAddSeats() { + if (this.shouldShowModal) { + this.showLimitedAccessModal = true; + return; + } + + this.trackClick(); + visitUrl(this.purchaseButtonLink); + }, }, }; </script> @@ -124,16 +146,17 @@ export default { </div> <gl-button v-if="purchaseButtonLink" - :href="purchaseButtonLink" + v-gl-modal-directive="'limited-access-modal-id'" category="primary" target="_blank" variant="confirm" class="gl-ml-3 gl-align-self-start" data-testid="purchase-button" data-qa-selector="add_seats" - @click="trackClick" + @click="handleAddSeats" > {{ $options.i18n.addSeatsText }} </gl-button> + <limited-access-modal v-if="shouldShowModal" v-model="showLimitedAccessModal" /> </div> </template> diff --git a/ee/app/controllers/ee/groups/usage_quotas_controller.rb b/ee/app/controllers/ee/groups/usage_quotas_controller.rb index 78e128f0d7fecee4e43c4a9c83b994907ad5616d..bb728f528edac18290841364987baeb5bb7e086e 100644 --- a/ee/app/controllers/ee/groups/usage_quotas_controller.rb +++ b/ee/app/controllers/ee/groups/usage_quotas_controller.rb @@ -14,6 +14,7 @@ module UsageQuotasController before_action only: [:index] do push_frontend_feature_flag(:data_transfer_monitoring, group) push_frontend_feature_flag(:enable_hamilton_in_usage_quotas_ui, group) + push_frontend_feature_flag(:limited_access_modal) end end diff --git a/ee/spec/frontend/usage_quotas/components/limited_access_modal_spec.js b/ee/spec/frontend/usage_quotas/components/limited_access_modal_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..7437e14a9cc88be697be16e15fd31d99dea5cfc6 --- /dev/null +++ b/ee/spec/frontend/usage_quotas/components/limited_access_modal_spec.js @@ -0,0 +1,47 @@ +import { GlModal } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import LimitedAccessModal from 'ee/usage_quotas/components/limited_access_modal.vue'; + +describe('LimitedAccessModal', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(LimitedAccessModal, { + propsData: { ...props }, + }); + }; + const findModal = () => wrapper.findComponent(GlModal); + + it('has correct button', () => { + createComponent(); + + expect(findModal().props('actionPrimary')).toStrictEqual({ + text: 'Close', + attributes: { variant: 'confirm' }, + }); + }); + + describe('with reseller', () => { + beforeEach(() => { + createComponent({ limitedAccessReason: 'MANAGED_BY_RESELLER' }); + }); + + it('shows correct content', () => { + const modal = findModal(); + + expect(modal.text()).toContain('GitLab Partner'); + }); + }); + + describe('with ramp', () => { + beforeEach(() => { + createComponent({ limitedAccessReason: 'RAMP_SUBSCRIPTION' }); + }); + + it('shows correct content', () => { + const modal = findModal(); + + expect(modal.text()).toContain('GitLab sales representative'); + }); + }); +}); diff --git a/ee/spec/frontend/usage_quotas/components/statistics_seats_card_spec.js b/ee/spec/frontend/usage_quotas/components/statistics_seats_card_spec.js index 96d565e36a7f5700dd7edeb61bed8bbb046049ab..79bee19ed0198784d8657f4e516a58daf2b61252 100644 --- a/ee/spec/frontend/usage_quotas/components/statistics_seats_card_spec.js +++ b/ee/spec/frontend/usage_quotas/components/statistics_seats_card_spec.js @@ -1,7 +1,15 @@ import { GlLink } from '@gitlab/ui'; +import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import StatisticsSeatsCard from 'ee/usage_quotas/seats/components/statistics_seats_card.vue'; import Tracking from '~/tracking'; +import { visitUrl } from '~/lib/utils/url_utility'; +import LimitedAccessModal from 'ee/usage_quotas/components/limited_access_modal.vue'; + +jest.mock('~/lib/utils/url_utility', () => ({ + ...jest.requireActual('~/lib/utils/url_utility'), + visitUrl: jest.fn().mockName('visitUrlMock'), +})); describe('StatisticsSeatsCard', () => { let wrapper; @@ -15,12 +23,16 @@ describe('StatisticsSeatsCard', () => { const createComponent = (props = {}) => { wrapper = shallowMountExtended(StatisticsSeatsCard, { propsData: { ...defaultProps, ...props }, + stubs: { + LimitedAccessModal, + }, }); }; const findSeatsUsedBlock = () => wrapper.findByTestId('seats-used-block'); const findSeatsOwedBlock = () => wrapper.findByTestId('seats-owed-block'); const findPurchaseButton = () => wrapper.findByTestId('purchase-button'); + const findLimitedAccessModal = () => wrapper.findComponent(LimitedAccessModal); describe('seats used block', () => { it('renders seats used block if seatsUsed is passed', () => { @@ -65,8 +77,6 @@ describe('StatisticsSeatsCard', () => { const purchaseButton = findPurchaseButton(); expect(purchaseButton.exists()).toBe(true); - expect(purchaseButton.attributes('href')).toBe(purchaseButtonLink); - expect(purchaseButton.attributes('target')).toBe('_blank'); }); it('does not render purchase button if purchase link is not passed', () => { @@ -85,5 +95,54 @@ describe('StatisticsSeatsCard', () => { property: 'usage_quotas_page', }); }); + + it('redirects when clicked', () => { + createComponent(); + findPurchaseButton().vm.$emit('click'); + + expect(visitUrl).toHaveBeenCalledWith('https://gitlab.com/purchase-more-seats'); + }); + }); + + describe('limited access modal', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when limitedAccessModal FF is on', () => { + beforeEach(async () => { + gon.features = { limitedAccessModal: true }; + createComponent(); + + findPurchaseButton().vm.$emit('click'); + await nextTick(); + }); + + it('shows modal', () => { + expect(findLimitedAccessModal().isVisible()).toBe(true); + }); + + it('does not navigate to URL', () => { + expect(visitUrl).not.toHaveBeenCalled(); + }); + }); + + describe('when limitedAccessModal FF is off', () => { + beforeEach(async () => { + gon.features = { limitedAccessModal: false }; + createComponent(); + + findPurchaseButton().vm.$emit('click'); + await nextTick(); + }); + + it('does not show modal', () => { + expect(findLimitedAccessModal().exists()).toBe(false); + }); + + it('navigates to URL', () => { + expect(visitUrl).toHaveBeenCalledWith('https://gitlab.com/purchase-more-seats'); + }); + }); }); }); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6303647a4d26136c50d1d80eeac72a0d154e2051..7ca4705cf810890229c8c077f2212158d9081a98 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -45791,6 +45791,18 @@ msgstr "" msgid "SubscriptionBanner|Upload new license" msgstr "" +msgid "SubscriptionMangement|If you'd like to add more seats, upgrade your plan, or purchase additional products, contact your GitLab sales representative." +msgstr "" + +msgid "SubscriptionMangement|This is a custom subscription managed by the GitLab Sales team" +msgstr "" + +msgid "SubscriptionMangement|To make changes to a read-only subscription or purchase additional products, contact your GitLab Partner." +msgstr "" + +msgid "SubscriptionMangement|Your subscription is in read-only mode" +msgstr "" + msgid "SubscriptionTable|Add seats" msgstr ""