diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb index 2e8ff1b7b497055520fa4bc8218501cc6e0c3c67..63727e45a5c400f4f1554d0b9af223713e432e50 100644 --- a/app/models/user_callout.rb +++ b/app/models/user_callout.rb @@ -31,7 +31,8 @@ class UserCallout < ApplicationRecord pipeline_needs_banner: 29, pipeline_needs_hover_tip: 30, web_ide_ci_environments_guidance: 31, - security_configuration_upgrade_banner: 32 + security_configuration_upgrade_banner: 32, + cloud_licensing_subscription_activation_banner: 33 # EE-only } validates :user, presence: true diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 80c809c334307484d6c186b04bc56be2bd5d5609..e564abd38c6eabb7e329f6838dcad40d13726436 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -15123,6 +15123,7 @@ Name of the feature that the callout is for. | <a id="usercalloutfeaturenameenumactive_user_count_threshold"></a>`ACTIVE_USER_COUNT_THRESHOLD` | Callout feature name for active_user_count_threshold. | | <a id="usercalloutfeaturenameenumbuy_pipeline_minutes_notification_dot"></a>`BUY_PIPELINE_MINUTES_NOTIFICATION_DOT` | Callout feature name for buy_pipeline_minutes_notification_dot. | | <a id="usercalloutfeaturenameenumcanary_deployment"></a>`CANARY_DEPLOYMENT` | Callout feature name for canary_deployment. | +| <a id="usercalloutfeaturenameenumcloud_licensing_subscription_activation_banner"></a>`CLOUD_LICENSING_SUBSCRIPTION_ACTIVATION_BANNER` | Callout feature name for cloud_licensing_subscription_activation_banner. | | <a id="usercalloutfeaturenameenumcluster_security_warning"></a>`CLUSTER_SECURITY_WARNING` | Callout feature name for cluster_security_warning. | | <a id="usercalloutfeaturenameenumcustomize_homepage"></a>`CUSTOMIZE_HOMEPAGE` | Callout feature name for customize_homepage. | | <a id="usercalloutfeaturenameenumeoa_bronze_plan_banner"></a>`EOA_BRONZE_PLAN_BANNER` | Callout feature name for eoa_bronze_plan_banner. | diff --git a/ee/app/assets/javascripts/admin/subscriptions/show/components/subscription_activation_banner.vue b/ee/app/assets/javascripts/admin/subscriptions/show/components/subscription_activation_banner.vue index 9cf4e4889b73942388ddaf41286d7ac946cda721..1ee537acae180da63cd0a366c4faf3161cbc164f 100644 --- a/ee/app/assets/javascripts/admin/subscriptions/show/components/subscription_activation_banner.vue +++ b/ee/app/assets/javascripts/admin/subscriptions/show/components/subscription_activation_banner.vue @@ -7,6 +7,7 @@ import { } from '../constants'; export const ACTIVATE_SUBSCRIPTION_EVENT = 'activate-subscription'; +export const CLOSE_ACTIVATE_SUBSCRIPTION_BANNER_EVENT = 'close'; export default { name: 'SubscriptionActivationBanner', @@ -22,6 +23,9 @@ export default { }, inject: ['congratulationSvgPath', 'customersPortalUrl'], methods: { + handleClose() { + this.$emit(CLOSE_ACTIVATE_SUBSCRIPTION_BANNER_EVENT); + }, handlePrimary() { this.$emit(ACTIVATE_SUBSCRIPTION_EVENT); }, @@ -35,6 +39,7 @@ export default { :title="$options.i18n.title" variant="promotion" :svg-path="congratulationSvgPath" + @close="handleClose" @primary="handlePrimary" > <p> diff --git a/ee/app/assets/javascripts/admin/subscriptions/show/components/subscription_breakdown.vue b/ee/app/assets/javascripts/admin/subscriptions/show/components/subscription_breakdown.vue index a7b05b50541a76b7247bed2f0823a3b961c3ebe2..8bcd356cb6e79f6208da97fc7d9bc22066b7a30b 100644 --- a/ee/app/assets/javascripts/admin/subscriptions/show/components/subscription_breakdown.vue +++ b/ee/app/assets/javascripts/admin/subscriptions/show/components/subscription_breakdown.vue @@ -1,6 +1,7 @@ <script> import { GlButton, GlModalDirective } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; +import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue'; import { activateCloudLicense, licensedToHeaderText, @@ -13,6 +14,7 @@ import { syncSubscriptionButtonText, uploadLicense, } from '../constants'; +import SubscriptionActivationBanner from './subscription_activation_banner.vue'; import SubscriptionActivationModal from './subscription_activation_modal.vue'; import SubscriptionDetailsCard from './subscription_details_card.vue'; import SubscriptionDetailsHistory from './subscription_details_history.vue'; @@ -41,14 +43,22 @@ export default { GlModal: GlModalDirective, }, components: { + SubscriptionActivationBanner, GlButton, SubscriptionActivationModal, SubscriptionDetailsCard, SubscriptionDetailsHistory, SubscriptionDetailsUserInfo, SubscriptionSyncNotifications: () => import('./subscription_sync_notifications.vue'), + UserCalloutDismisser, }, - inject: ['customersPortalUrl', 'licenseRemovePath', 'licenseUploadPath', 'subscriptionSyncPath'], + inject: [ + 'customersPortalUrl', + 'licenseRemovePath', + 'licenseUploadPath', + 'subscriptionSyncPath', + 'subscriptionActivationBannerCalloutName', + ], props: { subscription: { type: Object, @@ -117,6 +127,9 @@ export default { didDismissSuccessAlert() { this.shouldShowNotifications = false; }, + showActivationModal() { + this.activationModalVisible = true; + }, syncSubscription() { this.hasAsyncActivity = true; this.shouldShowNotifications = false; @@ -144,6 +157,19 @@ export default { v-model="activationModalVisible" :modal-id="$options.modal.id" /> + <user-callout-dismisser + v-if="canActivateSubscription" + :feature-name="subscriptionActivationBannerCalloutName" + > + <template #default="{ dismiss, shouldShowCallout }"> + <subscription-activation-banner + v-if="shouldShowCallout" + class="mb-4" + @activate-subscription="showActivationModal" + @close="dismiss" + /> + </template> + </user-callout-dismisser> <subscription-sync-notifications v-if="shouldShowNotifications" class="mb-4" @@ -158,6 +184,7 @@ export default { :header-text="$options.i18n.subscriptionDetailsHeaderText" :subscription="subscription" :sync-did-fail="syncDidFail" + data-testid="subscription-details" > <template v-if="shouldShowFooter" #footer> <gl-button diff --git a/ee/app/assets/javascripts/admin/subscriptions/show/mount_cloud_licenses.js b/ee/app/assets/javascripts/admin/subscriptions/show/mount_cloud_licenses.js index 96edb29fc71c0dfb3e18eda8beca166e1e936a52..d3ea5242df65a3645319d9e2ef7030bf85e2bcdf 100644 --- a/ee/app/assets/javascripts/admin/subscriptions/show/mount_cloud_licenses.js +++ b/ee/app/assets/javascripts/admin/subscriptions/show/mount_cloud_licenses.js @@ -31,6 +31,7 @@ export default () => { hasActiveLicense, licenseRemovePath, licenseUploadPath, + subscriptionActivationBannerCalloutName, subscriptionSyncPath, } = el.dataset; const connectivityHelpURL = helpPagePath('/user/admin_area/license.html', { @@ -48,6 +49,7 @@ export default () => { freeTrialPath, licenseRemovePath, licenseUploadPath, + subscriptionActivationBannerCalloutName, subscriptionSyncPath, }, render: (h) => diff --git a/ee/app/helpers/ee/user_callouts_helper.rb b/ee/app/helpers/ee/user_callouts_helper.rb index 64f5688d41c3a7bb0767a734802d5d55910f2b4d..63ce6b70b901f67272013b2111efa30321d9a7a7 100644 --- a/ee/app/helpers/ee/user_callouts_helper.rb +++ b/ee/app/helpers/ee/user_callouts_helper.rb @@ -13,6 +13,7 @@ module UserCalloutsHelper PERSONAL_ACCESS_TOKEN_EXPIRY = 'personal_access_token_expiry' EOA_BRONZE_PLAN_BANNER = 'eoa_bronze_plan_banner' EOA_BRONZE_PLAN_END_DATE = '2022-01-26' + CL_SUBSCRIPTION_ACTIVATION = 'cloud_licensing_subscription_activation_banner' def render_enable_hashed_storage_warning return unless show_enable_hashed_storage_warning? diff --git a/ee/app/helpers/license_helper.rb b/ee/app/helpers/license_helper.rb index d0dec654e1decd5169d0d5a556e85de4b1110088..ea5eeabb32961daf961d960789fc5efbcb825fd0 100644 --- a/ee/app/helpers/license_helper.rb +++ b/ee/app/helpers/license_helper.rb @@ -61,7 +61,8 @@ def cloud_license_view_data license_upload_path: new_admin_license_path, license_remove_path: admin_license_path, subscription_sync_path: sync_seat_link_admin_license_path, - congratulation_svg_path: image_path('illustrations/illustration-congratulation-purchase.svg') + congratulation_svg_path: image_path('illustrations/illustration-congratulation-purchase.svg'), + subscription_activation_banner_callout_name: ::EE::UserCalloutsHelper::CL_SUBSCRIPTION_ACTIVATION } end diff --git a/ee/spec/features/admin/subscriptions/admin_views_subscription_spec.rb b/ee/spec/features/admin/subscriptions/admin_views_subscription_spec.rb index fcc839569bf8801297eb29bc42d5d4c4811bfc37..09af830d66026fc0b71f8cdb0df309b5ea35525c 100644 --- a/ee/spec/features/admin/subscriptions/admin_views_subscription_spec.rb +++ b/ee/spec/features/admin/subscriptions/admin_views_subscription_spec.rb @@ -70,7 +70,9 @@ context 'when activating another subscription' do before do - click_button('Activate cloud license') + page.within(find('[data-testid="subscription-details"]', match: :first)) do + click_button('Activate cloud license') + end end it 'shows the activation modal' do diff --git a/ee/spec/frontend/admin/subscriptions/components/subscription_acivation_banner_spec.js b/ee/spec/frontend/admin/subscriptions/components/subscription_acivation_banner_spec.js index 5f539b38f1cfc70343d02423eac2c2b116fcbef4..c06c4b9c2ab8535445f0586585c0ca5472adc48d 100644 --- a/ee/spec/frontend/admin/subscriptions/components/subscription_acivation_banner_spec.js +++ b/ee/spec/frontend/admin/subscriptions/components/subscription_acivation_banner_spec.js @@ -2,6 +2,7 @@ import { GlBanner, GlLink, GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import SubscriptionActivationBanner, { ACTIVATE_SUBSCRIPTION_EVENT, + CLOSE_ACTIVATE_SUBSCRIPTION_BANNER_EVENT, } from 'ee/admin/subscriptions/show/components/subscription_activation_banner.vue'; import { activateCloudLicense, @@ -62,6 +63,14 @@ describe('SubscriptionActivationBanner', () => { findBanner().vm.$emit('primary'); - expect(wrapper.emitted(ACTIVATE_SUBSCRIPTION_EVENT)).toEqual([[]]); + expect(wrapper.emitted(ACTIVATE_SUBSCRIPTION_EVENT)).toHaveLength(1); + }); + + it('emits an event when the close button is clicked', () => { + expect(wrapper.emitted(CLOSE_ACTIVATE_SUBSCRIPTION_BANNER_EVENT)).toBeUndefined(); + + findBanner().vm.$emit('close'); + + expect(wrapper.emitted(CLOSE_ACTIVATE_SUBSCRIPTION_BANNER_EVENT)).toHaveLength(1); }); }); diff --git a/ee/spec/frontend/admin/subscriptions/components/subscription_breakdown_spec.js b/ee/spec/frontend/admin/subscriptions/components/subscription_breakdown_spec.js index f424929937aec74fbc359b476c1e732bef46dfb7..3ff3371b5bd1c633291354882a2758d96bd83da8 100644 --- a/ee/spec/frontend/admin/subscriptions/components/subscription_breakdown_spec.js +++ b/ee/spec/frontend/admin/subscriptions/components/subscription_breakdown_spec.js @@ -1,7 +1,10 @@ import { GlCard } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { mount, shallowMount } from '@vue/test-utils'; import AxiosMockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; +import SubscriptionActivationBanner, { + ACTIVATE_SUBSCRIPTION_EVENT, +} from 'ee/admin/subscriptions/show/components/subscription_activation_banner.vue'; import SubscriptionActivationModal from 'ee/admin/subscriptions/show/components/subscription_activation_modal.vue'; import SubscriptionBreakdown, { licensedToFields, @@ -20,6 +23,7 @@ import { subscriptionDetailsHeaderText, subscriptionTypes, } from 'ee/admin/subscriptions/show/constants'; +import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import axios from '~/lib/utils/axios_utils'; @@ -29,12 +33,15 @@ describe('Subscription Breakdown', () => { let axiosMock; let wrapper; let glModalDirective; + let userCalloutDismissSpy; const [, licenseFile] = subscriptionHistory; + const congratulationSvgPath = '/path/to/svg'; const connectivityHelpURL = 'connectivity/help/url'; const customersPortalUrl = 'customers.dot'; const licenseRemovePath = '/license/remove/'; const licenseUploadPath = '/license/upload/'; + const subscriptionActivationBannerCalloutName = 'banner_callout_name'; const subscriptionSyncPath = '/sync/path/'; const findDetailsCards = () => wrapper.findAllComponents(SubscriptionDetailsCard); @@ -47,14 +54,23 @@ describe('Subscription Breakdown', () => { wrapper.findByTestId('subscription-activate-subscription-action'); const findSubscriptionMangeAction = () => wrapper.findByTestId('subscription-manage-action'); const findSubscriptionSyncAction = () => wrapper.findByTestId('subscription-sync-action'); + const findSubscriptionActivationBanner = () => + wrapper.findComponent(SubscriptionActivationBanner); const findSubscriptionActivationModal = () => wrapper.findComponent(SubscriptionActivationModal); const findSubscriptionSyncNotifications = () => wrapper.findComponent(SubscriptionSyncNotifications); - const createComponent = ({ props = {}, provide = {}, stubs = {} } = {}) => { + const createComponent = ({ + props = {}, + provide = {}, + stubs = {}, + mountMethod = shallowMount, + shouldShowCallout = true, + } = {}) => { glModalDirective = jest.fn(); + userCalloutDismissSpy = jest.fn(); wrapper = extendedWrapper( - shallowMount(SubscriptionBreakdown, { + mountMethod(SubscriptionBreakdown, { directives: { glModal: { bind(_, { value }) { @@ -63,10 +79,12 @@ describe('Subscription Breakdown', () => { }, }, provide: { + congratulationSvgPath, connectivityHelpURL, customersPortalUrl, licenseUploadPath, licenseRemovePath, + subscriptionActivationBannerCalloutName, subscriptionSyncPath, ...provide, }, @@ -75,7 +93,13 @@ describe('Subscription Breakdown', () => { subscriptionList: subscriptionHistory, ...props, }, - stubs, + stubs: { + UserCalloutDismisser: makeMockUserCalloutDismisser({ + dismiss: userCalloutDismissSpy, + shouldShowCallout, + }), + ...stubs, + }, }), ); }; @@ -152,6 +176,10 @@ describe('Subscription Breakdown', () => { expect(findSubscriptionActivationModal().props('visible')).toBe(true); }); + it('does not present a subscription activation banner', () => { + expect(findSubscriptionActivationBanner().exists()).toBe(false); + }); + describe('footer buttons', () => { it.each` url | type | shouldShow @@ -270,7 +298,10 @@ describe('Subscription Breakdown', () => { beforeEach(() => { createComponent({ props: { subscription: licenseFile }, - stubs: { GlCard, SubscriptionDetailsCard }, + stubs: { + GlCard, + SubscriptionDetailsCard, + }, }); }); @@ -291,6 +322,42 @@ describe('Subscription Breakdown', () => { expect(glModalDirective).toHaveBeenCalledWith(modalId); }); + + describe('subscription activation banner', () => { + beforeEach(() => { + createComponent({ + props: { subscription: licenseFile }, + }); + }); + + it('presents a subscription activation banner', () => { + expect(findSubscriptionActivationBanner().exists()).toBe(true); + }); + + it('calls the dismiss callback when closing the banner', () => { + findSubscriptionActivationBanner().vm.$emit('close'); + + expect(userCalloutDismissSpy).toHaveBeenCalledTimes(1); + }); + + it('shows a modal', async () => { + expect(findSubscriptionActivationModal().props('visible')).toBe(false); + + await findSubscriptionActivationBanner().vm.$emit(ACTIVATE_SUBSCRIPTION_EVENT); + + expect(findSubscriptionActivationModal().props('visible')).toBe(true); + }); + + it('hides the banner when the proper condition applies', () => { + createComponent({ + mountMethod: mount, + props: { subscription: licenseFile }, + shouldShowCallout: false, + }); + + expect(findSubscriptionActivationBanner().exists()).toBe(false); + }); + }); }); describe('sync a subscription success', () => { diff --git a/ee/spec/helpers/license_helper_spec.rb b/ee/spec/helpers/license_helper_spec.rb index 347833d1be3ee24a4dfddf21dd6bdf906733e327..21a78b6110a814df8e160c92567513abb5b16164 100644 --- a/ee/spec/helpers/license_helper_spec.rb +++ b/ee/spec/helpers/license_helper_spec.rb @@ -100,7 +100,8 @@ def stub_default_url_options(host: "localhost", protocol: "http", port: nil, scr subscription_sync_path: sync_seat_link_admin_license_path, license_upload_path: new_admin_license_path, license_remove_path: admin_license_path, - congratulation_svg_path: helper.image_path('illustrations/illustration-congratulation-purchase.svg') }) + congratulation_svg_path: helper.image_path('illustrations/illustration-congratulation-purchase.svg'), + subscription_activation_banner_callout_name: ::EE::UserCalloutsHelper::CL_SUBSCRIPTION_ACTIVATION }) end end @@ -115,7 +116,8 @@ def stub_default_url_options(host: "localhost", protocol: "http", port: nil, scr subscription_sync_path: sync_seat_link_admin_license_path, license_upload_path: new_admin_license_path, license_remove_path: admin_license_path, - congratulation_svg_path: helper.image_path('illustrations/illustration-congratulation-purchase.svg') }) + congratulation_svg_path: helper.image_path('illustrations/illustration-congratulation-purchase.svg'), + subscription_activation_banner_callout_name: ::EE::UserCalloutsHelper::CL_SUBSCRIPTION_ACTIVATION }) end end end