Commit 5bcd609d authored by Jacques Erasmus's avatar Jacques Erasmus

Merge branch 'ag/332478-mid-term-banner-callout' into 'master'

Make Subscription Activation Banner dismissible

See merge request gitlab-org/gitlab!64618
parents 780d9024 c4b05c68
...@@ -31,7 +31,8 @@ class UserCallout < ApplicationRecord ...@@ -31,7 +31,8 @@ class UserCallout < ApplicationRecord
pipeline_needs_banner: 29, pipeline_needs_banner: 29,
pipeline_needs_hover_tip: 30, pipeline_needs_hover_tip: 30,
web_ide_ci_environments_guidance: 31, 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 validates :user, presence: true
......
...@@ -15211,6 +15211,7 @@ Name of the feature that the callout is for. ...@@ -15211,6 +15211,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="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="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="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="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="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. | | <a id="usercalloutfeaturenameenumeoa_bronze_plan_banner"></a>`EOA_BRONZE_PLAN_BANNER` | Callout feature name for eoa_bronze_plan_banner. |
......
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
} from '../constants'; } from '../constants';
export const ACTIVATE_SUBSCRIPTION_EVENT = 'activate-subscription'; export const ACTIVATE_SUBSCRIPTION_EVENT = 'activate-subscription';
export const CLOSE_ACTIVATE_SUBSCRIPTION_BANNER_EVENT = 'close';
export default { export default {
name: 'SubscriptionActivationBanner', name: 'SubscriptionActivationBanner',
...@@ -22,6 +23,9 @@ export default { ...@@ -22,6 +23,9 @@ export default {
}, },
inject: ['congratulationSvgPath', 'customersPortalUrl'], inject: ['congratulationSvgPath', 'customersPortalUrl'],
methods: { methods: {
handleClose() {
this.$emit(CLOSE_ACTIVATE_SUBSCRIPTION_BANNER_EVENT);
},
handlePrimary() { handlePrimary() {
this.$emit(ACTIVATE_SUBSCRIPTION_EVENT); this.$emit(ACTIVATE_SUBSCRIPTION_EVENT);
}, },
...@@ -35,6 +39,7 @@ export default { ...@@ -35,6 +39,7 @@ export default {
:title="$options.i18n.title" :title="$options.i18n.title"
variant="promotion" variant="promotion"
:svg-path="congratulationSvgPath" :svg-path="congratulationSvgPath"
@close="handleClose"
@primary="handlePrimary" @primary="handlePrimary"
> >
<p> <p>
......
<script> <script>
import { GlButton, GlModalDirective } from '@gitlab/ui'; import { GlButton, GlModalDirective } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import { import {
activateCloudLicense, activateCloudLicense,
licensedToHeaderText, licensedToHeaderText,
...@@ -13,6 +14,7 @@ import { ...@@ -13,6 +14,7 @@ import {
syncSubscriptionButtonText, syncSubscriptionButtonText,
uploadLicense, uploadLicense,
} from '../constants'; } from '../constants';
import SubscriptionActivationBanner from './subscription_activation_banner.vue';
import SubscriptionActivationModal from './subscription_activation_modal.vue'; import SubscriptionActivationModal from './subscription_activation_modal.vue';
import SubscriptionDetailsCard from './subscription_details_card.vue'; import SubscriptionDetailsCard from './subscription_details_card.vue';
import SubscriptionDetailsHistory from './subscription_details_history.vue'; import SubscriptionDetailsHistory from './subscription_details_history.vue';
...@@ -41,14 +43,22 @@ export default { ...@@ -41,14 +43,22 @@ export default {
GlModal: GlModalDirective, GlModal: GlModalDirective,
}, },
components: { components: {
SubscriptionActivationBanner,
GlButton, GlButton,
SubscriptionActivationModal, SubscriptionActivationModal,
SubscriptionDetailsCard, SubscriptionDetailsCard,
SubscriptionDetailsHistory, SubscriptionDetailsHistory,
SubscriptionDetailsUserInfo, SubscriptionDetailsUserInfo,
SubscriptionSyncNotifications: () => import('./subscription_sync_notifications.vue'), SubscriptionSyncNotifications: () => import('./subscription_sync_notifications.vue'),
UserCalloutDismisser,
}, },
inject: ['customersPortalUrl', 'licenseRemovePath', 'licenseUploadPath', 'subscriptionSyncPath'], inject: [
'customersPortalUrl',
'licenseRemovePath',
'licenseUploadPath',
'subscriptionSyncPath',
'subscriptionActivationBannerCalloutName',
],
props: { props: {
subscription: { subscription: {
type: Object, type: Object,
...@@ -117,6 +127,9 @@ export default { ...@@ -117,6 +127,9 @@ export default {
didDismissSuccessAlert() { didDismissSuccessAlert() {
this.shouldShowNotifications = false; this.shouldShowNotifications = false;
}, },
showActivationModal() {
this.activationModalVisible = true;
},
syncSubscription() { syncSubscription() {
this.hasAsyncActivity = true; this.hasAsyncActivity = true;
this.shouldShowNotifications = false; this.shouldShowNotifications = false;
...@@ -144,6 +157,19 @@ export default { ...@@ -144,6 +157,19 @@ export default {
v-model="activationModalVisible" v-model="activationModalVisible"
:modal-id="$options.modal.id" :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 <subscription-sync-notifications
v-if="shouldShowNotifications" v-if="shouldShowNotifications"
class="mb-4" class="mb-4"
...@@ -158,6 +184,7 @@ export default { ...@@ -158,6 +184,7 @@ export default {
:header-text="$options.i18n.subscriptionDetailsHeaderText" :header-text="$options.i18n.subscriptionDetailsHeaderText"
:subscription="subscription" :subscription="subscription"
:sync-did-fail="syncDidFail" :sync-did-fail="syncDidFail"
data-testid="subscription-details"
> >
<template v-if="shouldShowFooter" #footer> <template v-if="shouldShowFooter" #footer>
<gl-button <gl-button
......
...@@ -31,6 +31,7 @@ export default () => { ...@@ -31,6 +31,7 @@ export default () => {
hasActiveLicense, hasActiveLicense,
licenseRemovePath, licenseRemovePath,
licenseUploadPath, licenseUploadPath,
subscriptionActivationBannerCalloutName,
subscriptionSyncPath, subscriptionSyncPath,
} = el.dataset; } = el.dataset;
const connectivityHelpURL = helpPagePath('/user/admin_area/license.html', { const connectivityHelpURL = helpPagePath('/user/admin_area/license.html', {
...@@ -48,6 +49,7 @@ export default () => { ...@@ -48,6 +49,7 @@ export default () => {
freeTrialPath, freeTrialPath,
licenseRemovePath, licenseRemovePath,
licenseUploadPath, licenseUploadPath,
subscriptionActivationBannerCalloutName,
subscriptionSyncPath, subscriptionSyncPath,
}, },
render: (h) => render: (h) =>
......
...@@ -13,6 +13,7 @@ module EE ...@@ -13,6 +13,7 @@ module EE
PERSONAL_ACCESS_TOKEN_EXPIRY = 'personal_access_token_expiry' PERSONAL_ACCESS_TOKEN_EXPIRY = 'personal_access_token_expiry'
EOA_BRONZE_PLAN_BANNER = 'eoa_bronze_plan_banner' EOA_BRONZE_PLAN_BANNER = 'eoa_bronze_plan_banner'
EOA_BRONZE_PLAN_END_DATE = '2022-01-26' EOA_BRONZE_PLAN_END_DATE = '2022-01-26'
CL_SUBSCRIPTION_ACTIVATION = 'cloud_licensing_subscription_activation_banner'
def render_enable_hashed_storage_warning def render_enable_hashed_storage_warning
return unless show_enable_hashed_storage_warning? return unless show_enable_hashed_storage_warning?
......
...@@ -61,7 +61,8 @@ module LicenseHelper ...@@ -61,7 +61,8 @@ module LicenseHelper
license_upload_path: new_admin_license_path, license_upload_path: new_admin_license_path,
license_remove_path: admin_license_path, license_remove_path: admin_license_path,
subscription_sync_path: sync_seat_link_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 end
......
...@@ -70,7 +70,9 @@ RSpec.describe 'Admin views Subscription', :js do ...@@ -70,7 +70,9 @@ RSpec.describe 'Admin views Subscription', :js do
context 'when activating another subscription' do context 'when activating another subscription' do
before 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 end
it 'shows the activation modal' do it 'shows the activation modal' do
......
...@@ -2,6 +2,7 @@ import { GlBanner, GlLink, GlSprintf } from '@gitlab/ui'; ...@@ -2,6 +2,7 @@ import { GlBanner, GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import SubscriptionActivationBanner, { import SubscriptionActivationBanner, {
ACTIVATE_SUBSCRIPTION_EVENT, ACTIVATE_SUBSCRIPTION_EVENT,
CLOSE_ACTIVATE_SUBSCRIPTION_BANNER_EVENT,
} from 'ee/admin/subscriptions/show/components/subscription_activation_banner.vue'; } from 'ee/admin/subscriptions/show/components/subscription_activation_banner.vue';
import { import {
activateCloudLicense, activateCloudLicense,
...@@ -62,6 +63,14 @@ describe('SubscriptionActivationBanner', () => { ...@@ -62,6 +63,14 @@ describe('SubscriptionActivationBanner', () => {
findBanner().vm.$emit('primary'); 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 { GlCard } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter'; import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue'; 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 SubscriptionActivationModal from 'ee/admin/subscriptions/show/components/subscription_activation_modal.vue';
import SubscriptionBreakdown, { import SubscriptionBreakdown, {
licensedToFields, licensedToFields,
...@@ -20,6 +23,7 @@ import { ...@@ -20,6 +23,7 @@ import {
subscriptionDetailsHeaderText, subscriptionDetailsHeaderText,
subscriptionTypes, subscriptionTypes,
} from 'ee/admin/subscriptions/show/constants'; } from 'ee/admin/subscriptions/show/constants';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
...@@ -29,12 +33,15 @@ describe('Subscription Breakdown', () => { ...@@ -29,12 +33,15 @@ describe('Subscription Breakdown', () => {
let axiosMock; let axiosMock;
let wrapper; let wrapper;
let glModalDirective; let glModalDirective;
let userCalloutDismissSpy;
const [, licenseFile] = subscriptionHistory; const [, licenseFile] = subscriptionHistory;
const congratulationSvgPath = '/path/to/svg';
const connectivityHelpURL = 'connectivity/help/url'; const connectivityHelpURL = 'connectivity/help/url';
const customersPortalUrl = 'customers.dot'; const customersPortalUrl = 'customers.dot';
const licenseRemovePath = '/license/remove/'; const licenseRemovePath = '/license/remove/';
const licenseUploadPath = '/license/upload/'; const licenseUploadPath = '/license/upload/';
const subscriptionActivationBannerCalloutName = 'banner_callout_name';
const subscriptionSyncPath = '/sync/path/'; const subscriptionSyncPath = '/sync/path/';
const findDetailsCards = () => wrapper.findAllComponents(SubscriptionDetailsCard); const findDetailsCards = () => wrapper.findAllComponents(SubscriptionDetailsCard);
...@@ -47,14 +54,23 @@ describe('Subscription Breakdown', () => { ...@@ -47,14 +54,23 @@ describe('Subscription Breakdown', () => {
wrapper.findByTestId('subscription-activate-subscription-action'); wrapper.findByTestId('subscription-activate-subscription-action');
const findSubscriptionMangeAction = () => wrapper.findByTestId('subscription-manage-action'); const findSubscriptionMangeAction = () => wrapper.findByTestId('subscription-manage-action');
const findSubscriptionSyncAction = () => wrapper.findByTestId('subscription-sync-action'); const findSubscriptionSyncAction = () => wrapper.findByTestId('subscription-sync-action');
const findSubscriptionActivationBanner = () =>
wrapper.findComponent(SubscriptionActivationBanner);
const findSubscriptionActivationModal = () => wrapper.findComponent(SubscriptionActivationModal); const findSubscriptionActivationModal = () => wrapper.findComponent(SubscriptionActivationModal);
const findSubscriptionSyncNotifications = () => const findSubscriptionSyncNotifications = () =>
wrapper.findComponent(SubscriptionSyncNotifications); wrapper.findComponent(SubscriptionSyncNotifications);
const createComponent = ({ props = {}, provide = {}, stubs = {} } = {}) => { const createComponent = ({
props = {},
provide = {},
stubs = {},
mountMethod = shallowMount,
shouldShowCallout = true,
} = {}) => {
glModalDirective = jest.fn(); glModalDirective = jest.fn();
userCalloutDismissSpy = jest.fn();
wrapper = extendedWrapper( wrapper = extendedWrapper(
shallowMount(SubscriptionBreakdown, { mountMethod(SubscriptionBreakdown, {
directives: { directives: {
glModal: { glModal: {
bind(_, { value }) { bind(_, { value }) {
...@@ -63,10 +79,12 @@ describe('Subscription Breakdown', () => { ...@@ -63,10 +79,12 @@ describe('Subscription Breakdown', () => {
}, },
}, },
provide: { provide: {
congratulationSvgPath,
connectivityHelpURL, connectivityHelpURL,
customersPortalUrl, customersPortalUrl,
licenseUploadPath, licenseUploadPath,
licenseRemovePath, licenseRemovePath,
subscriptionActivationBannerCalloutName,
subscriptionSyncPath, subscriptionSyncPath,
...provide, ...provide,
}, },
...@@ -75,7 +93,13 @@ describe('Subscription Breakdown', () => { ...@@ -75,7 +93,13 @@ describe('Subscription Breakdown', () => {
subscriptionList: subscriptionHistory, subscriptionList: subscriptionHistory,
...props, ...props,
}, },
stubs, stubs: {
UserCalloutDismisser: makeMockUserCalloutDismisser({
dismiss: userCalloutDismissSpy,
shouldShowCallout,
}),
...stubs,
},
}), }),
); );
}; };
...@@ -152,6 +176,10 @@ describe('Subscription Breakdown', () => { ...@@ -152,6 +176,10 @@ describe('Subscription Breakdown', () => {
expect(findSubscriptionActivationModal().props('visible')).toBe(true); expect(findSubscriptionActivationModal().props('visible')).toBe(true);
}); });
it('does not present a subscription activation banner', () => {
expect(findSubscriptionActivationBanner().exists()).toBe(false);
});
describe('footer buttons', () => { describe('footer buttons', () => {
it.each` it.each`
url | type | shouldShow url | type | shouldShow
...@@ -270,7 +298,10 @@ describe('Subscription Breakdown', () => { ...@@ -270,7 +298,10 @@ describe('Subscription Breakdown', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
props: { subscription: licenseFile }, props: { subscription: licenseFile },
stubs: { GlCard, SubscriptionDetailsCard }, stubs: {
GlCard,
SubscriptionDetailsCard,
},
}); });
}); });
...@@ -291,6 +322,42 @@ describe('Subscription Breakdown', () => { ...@@ -291,6 +322,42 @@ describe('Subscription Breakdown', () => {
expect(glModalDirective).toHaveBeenCalledWith(modalId); 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', () => { describe('sync a subscription success', () => {
......
...@@ -100,7 +100,8 @@ RSpec.describe LicenseHelper do ...@@ -100,7 +100,8 @@ RSpec.describe LicenseHelper do
subscription_sync_path: sync_seat_link_admin_license_path, subscription_sync_path: sync_seat_link_admin_license_path,
license_upload_path: new_admin_license_path, license_upload_path: new_admin_license_path,
license_remove_path: 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 end
...@@ -115,7 +116,8 @@ RSpec.describe LicenseHelper do ...@@ -115,7 +116,8 @@ RSpec.describe LicenseHelper do
subscription_sync_path: sync_seat_link_admin_license_path, subscription_sync_path: sync_seat_link_admin_license_path,
license_upload_path: new_admin_license_path, license_upload_path: new_admin_license_path,
license_remove_path: 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 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