Commit c4b05c68 authored by Angelo Gulina's avatar Angelo Gulina Committed by Jacques Erasmus

Make Subscription Activation Banner dismissible

parent ece9a17c
......@@ -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
......
......@@ -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. |
......
......@@ -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>
......
<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
......
......@@ -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) =>
......
......@@ -13,6 +13,7 @@ module EE
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?
......
......@@ -61,7 +61,8 @@ module LicenseHelper
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
......
......@@ -70,7 +70,9 @@ RSpec.describe 'Admin views Subscription', :js do
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
......
......@@ -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);
});
});
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', () => {
......
......@@ -100,7 +100,8 @@ RSpec.describe LicenseHelper do
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 @@ RSpec.describe LicenseHelper do
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
......
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