Commit 7fa25e26 authored by Ammar Alakkad's avatar Ammar Alakkad Committed by Michael Lunøe

Adds a notification for a future dated license

When an instance has no currently active subscription, but has a future
dated subscription — we display a notification to ensure them that they
don't need to do anything else to activate it. And the Subscription
History table is now shown whether there is active subscription or not.
Placing it above "Free trial" and "Subscription" cards if there's a
future dated subscription, and under them if not.

Changelog: changed
EE: true
parent fac71f1e
......@@ -3,8 +3,6 @@ import { GlAlert, GlButton, GlSprintf } from '@gitlab/ui';
import { isInFuture } from '~/lib/utils/datetime/date_calculation_utility';
import { sprintf } from '~/locale';
import {
activateSubscription,
noActiveSubscription,
subscriptionActivationNotificationText,
subscriptionActivationFutureDatedNotificationTitle,
subscriptionActivationFutureDatedNotificationMessage,
......@@ -20,10 +18,8 @@ import {
import getCurrentLicense from '../graphql/queries/get_current_license.query.graphql';
import getPastLicenseHistory from '../graphql/queries/get_past_license_history.query.graphql';
import getFutureLicenseHistory from '../graphql/queries/get_future_license_history.query.graphql';
import SubscriptionActivationCard from './subscription_activation_card.vue';
import SubscriptionBreakdown from './subscription_breakdown.vue';
import SubscriptionPurchaseCard from './subscription_purchase_card.vue';
import SubscriptionTrialCard from './subscription_trial_card.vue';
import NoActiveSubscription from './no_active_subscription.vue';
export default {
name: 'CloudLicenseApp',
......@@ -31,15 +27,11 @@ export default {
GlAlert,
GlButton,
GlSprintf,
SubscriptionActivationCard,
SubscriptionBreakdown,
SubscriptionPurchaseCard,
SubscriptionTrialCard,
NoActiveSubscription,
},
i18n: {
activateSubscription,
exportLicenseUsageBtnText,
noActiveSubscription,
subscriptionMainTitle,
subscriptionHistoryFailedTitle,
subscriptionHistoryFailedMessage,
......@@ -176,21 +168,10 @@ export default {
:subscription-list="subscriptionHistory"
v-on="$options.activationListeners"
/>
<div v-else class="row">
<div class="col-12">
<h3 class="gl-mb-7 gl-mt-6 gl-text-center" data-testid="subscription-activation-title">
{{ $options.i18n.noActiveSubscription }}
</h3>
<subscription-activation-card v-on="$options.activationListeners" />
<div class="row gl-mt-7">
<div class="col-lg-6">
<subscription-trial-card />
</div>
<div class="col-lg-6">
<subscription-purchase-card />
</div>
</div>
</div>
</div>
<no-active-subscription
v-else
:subscription-list="subscriptionHistory"
v-on="$options.activationListeners"
/>
</div>
</template>
<script>
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { minBy } from 'lodash';
import { isInFuture } from '~/lib/utils/datetime/date_calculation_utility';
import { instanceHasFutureLicenseBanner, noActiveSubscription } from '../constants';
import SubscriptionActivationCard from './subscription_activation_card.vue';
import SubscriptionDetailsHistory from './subscription_details_history.vue';
import SubscriptionPurchaseCard from './subscription_purchase_card.vue';
import SubscriptionTrialCard from './subscription_trial_card.vue';
export default {
name: 'NoActiveSubscription',
components: {
GlAlert,
GlSprintf,
SubscriptionActivationCard,
SubscriptionPurchaseCard,
SubscriptionTrialCard,
SubscriptionDetailsHistory,
},
i18n: {
instanceHasFutureLicenseBanner,
noActiveSubscription,
},
props: {
subscriptionList: {
type: Array,
required: true,
},
},
computed: {
hasItems() {
return Boolean(this.subscriptionList.length);
},
nextFutureDatedLicenseDate() {
const futureItems = this.subscriptionList.filter((license) =>
isInFuture(new Date(license.startsAt)),
);
const nextFutureDatedItem = minBy(futureItems, (license) => new Date(license.startsAt));
return nextFutureDatedItem?.startsAt;
},
hasFutureDatedLicense() {
return Boolean(this.nextFutureDatedLicenseDate);
},
},
};
</script>
<template>
<div class="row">
<div class="col-12">
<h3 class="gl-mb-7 gl-mt-6 gl-text-center" data-testid="subscription-activation-title">
{{ $options.i18n.noActiveSubscription }}
</h3>
<subscription-activation-card v-on="$listeners" />
<gl-alert
v-if="hasFutureDatedLicense"
:title="$options.i18n.instanceHasFutureLicenseBanner.title"
:dismissible="false"
class="gl-mt-5"
variant="info"
data-testid="subscription-future-licenses-alert"
>
<gl-sprintf :message="$options.i18n.instanceHasFutureLicenseBanner.message">
<template #date>{{ nextFutureDatedLicenseDate }}</template>
</gl-sprintf>
</gl-alert>
<div v-if="hasItems && hasFutureDatedLicense" class="col-12 gl-mt-5">
<subscription-details-history :subscription-list="subscriptionList" />
</div>
<div class="row gl-mt-7">
<div class="col-lg-6 gl-sm-mb-7">
<subscription-trial-card />
</div>
<div class="col-lg-6">
<subscription-purchase-card />
</div>
</div>
<div v-if="hasItems && !hasFutureDatedLicense" class="col-12 gl-mt-5">
<subscription-details-history :subscription-list="subscriptionList" />
</div>
</div>
</div>
</template>
......@@ -158,3 +158,10 @@ export const subscriptionBannerText = s__(
export const subscriptionBannerBlogPostUrl =
'https://about.gitlab.com/blog/2021/07/20/improved-billing-and-subscription-management/';
export const exportLicenseUsageBtnText = s__('SuperSonics|Export license usage file');
export const instanceHasFutureLicenseBanner = {
title: s__('SuperSonics|You have a future dated license'),
message: s__(
'SuperSonics|You have added a license that activates on %{date}. Please see the subscription history table below for more details.',
),
};
import { GlButton, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import SubscriptionManagementApp from 'ee/admin/subscriptions/show/components/app.vue';
import SubscriptionActivationCard from 'ee/admin/subscriptions/show/components/subscription_activation_card.vue';
import SubscriptionBreakdown from 'ee/admin/subscriptions/show/components/subscription_breakdown.vue';
import NoActiveSubscription from 'ee_else_ce/admin/subscriptions/show/components/no_active_subscription.vue';
import {
noActiveSubscription,
subscriptionActivationNotificationText,
subscriptionActivationFutureDatedNotificationTitle,
subscriptionHistoryFailedTitle,
......@@ -35,10 +34,8 @@ describe('SubscriptionManagementApp', () => {
let wrapper;
const findActivateSubscriptionCard = () => wrapper.findComponent(SubscriptionActivationCard);
const findSubscriptionBreakdown = () => wrapper.findComponent(SubscriptionBreakdown);
const findSubscriptionActivationTitle = () =>
wrapper.findByTestId('subscription-activation-title');
const findNoActiveSubscription = () => wrapper.findComponent(NoActiveSubscription);
const findSubscriptionMainTitle = () => wrapper.findByTestId('subscription-main-title');
const findSubscriptionActivationSuccessAlert = () =>
wrapper.findByTestId('subscription-activation-success-alert');
......@@ -77,8 +74,8 @@ describe('SubscriptionManagementApp', () => {
wrapper.destroy();
});
describe('when failing to fetch subcriptions', () => {
describe('when failing to fetch history subcriptions', () => {
describe('when failing to fetch subscriptions', () => {
describe('when failing to fetch history subscriptions', () => {
describe.each`
currentFails | pastFails | futureFails
${true} | ${false} | ${false}
......@@ -139,7 +136,6 @@ describe('SubscriptionManagementApp', () => {
});
});
describe('Subscription Activation Form', () => {
it('shows the main title', () => {
currentSubscriptionResolver = jest
.fn()
......@@ -158,26 +154,34 @@ describe('SubscriptionManagementApp', () => {
expect(findSubscriptionMainTitle().text()).toBe(subscriptionMainTitle);
});
describe('Subscription Activation Form', () => {
describe('without an active license', () => {
beforeEach(() => {
beforeEach(async () => {
currentSubscriptionResolver = jest
.fn()
.mockResolvedValue({ data: { currentLicense: null } });
pastSubscriptionsResolver = jest
.fn()
.mockResolvedValue({ data: { licenseHistoryEntries: { nodes: [] } } });
futureSubscriptionsResolver = jest
.fn()
.mockResolvedValue({ data: { subscriptionFutureEntries: { nodes: [] } } });
createComponent({}, [
pastSubscriptionsResolver = jest.fn().mockResolvedValue({
data: { licenseHistoryEntries: { nodes: subscriptionPastHistory } },
});
futureSubscriptionsResolver = jest.fn().mockResolvedValue({
data: { subscriptionFutureEntries: { nodes: subscriptionFutureHistory } },
});
createComponent({ hasActiveLicense: false }, [
currentSubscriptionResolver,
pastSubscriptionsResolver,
futureSubscriptionsResolver,
]);
await waitForPromises();
});
it('shows a title saying there is no active subscription', () => {
expect(findSubscriptionActivationTitle().text()).toBe(noActiveSubscription);
it('shows the no active subscription state', () => {
expect(findNoActiveSubscription().exists()).toBe(true);
});
it('passes correct data to the no subscription state', () => {
expect(findNoActiveSubscription().props()).toMatchObject({
subscriptionList: [...subscriptionFutureHistory, ...subscriptionPastHistory],
});
});
it('queries for the past history', () => {
......@@ -188,10 +192,6 @@ describe('SubscriptionManagementApp', () => {
expect(futureSubscriptionsResolver).toHaveBeenCalledTimes(1);
});
it('shows the subscription activation form', () => {
expect(findActivateSubscriptionCard().exists()).toBe(true);
});
it('does not show the activation success notification', () => {
expect(findSubscriptionActivationSuccessAlert().exists()).toBe(false);
});
......@@ -202,11 +202,11 @@ describe('SubscriptionManagementApp', () => {
describe('activating the license', () => {
it('shows the activation success notification', async () => {
findActivateSubscriptionCard().vm.$emit(
findNoActiveSubscription().vm.$emit(
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
license.ULTIMATE,
);
await waitForPromises();
await nextTick();
expect(findSubscriptionActivationSuccessAlert().props('title')).toBe(
subscriptionActivationNotificationText,
......@@ -214,10 +214,12 @@ describe('SubscriptionManagementApp', () => {
});
it('shows the future dated activation success notification', async () => {
await findActivateSubscriptionCard().vm.$emit(
findNoActiveSubscription().vm.$emit(
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
license.ULTIMATE_FUTURE_DATED,
);
await nextTick();
expect(findSubscriptionActivationSuccessAlert().props('title')).toBe(
subscriptionActivationFutureDatedNotificationTitle,
);
......@@ -261,10 +263,12 @@ describe('SubscriptionManagementApp', () => {
});
it('shows the activation success notification', async () => {
await findSubscriptionBreakdown().vm.$emit(
findSubscriptionBreakdown().vm.$emit(
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
license.ULTIMATE,
);
await nextTick();
expect(findSubscriptionActivationSuccessAlert().props('title')).toBe(
subscriptionActivationNotificationText,
);
......@@ -281,10 +285,12 @@ describe('SubscriptionManagementApp', () => {
});
it('calls refetch to update local state', async () => {
await findSubscriptionBreakdown().vm.$emit(
findSubscriptionBreakdown().vm.$emit(
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
license.ULTIMATE_FUTURE_DATED,
);
await nextTick();
expect(wrapper.vm.$apollo.queries.currentSubscription.refetch).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$apollo.queries.pastLicenseHistoryEntries.refetch).toHaveBeenCalledTimes(
1,
......@@ -333,7 +339,7 @@ describe('SubscriptionManagementApp', () => {
});
});
it('does not the activation success notification', () => {
it('does not show the activation success notification', () => {
expect(findSubscriptionActivationSuccessAlert().exists()).toBe(false);
});
......
import { GlSprintf } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import SubscriptionActivationCard from 'ee/admin/subscriptions/show/components/subscription_activation_card.vue';
import SubscriptionDetailsHistory from 'ee/admin/subscriptions/show/components/subscription_details_history.vue';
import NoActiveSubscription from 'ee_else_ce/admin/subscriptions/show/components/no_active_subscription.vue';
import { isInFuture } from '~/lib/utils/datetime/date_calculation_utility';
import {
instanceHasFutureLicenseBanner,
noActiveSubscription,
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
} from 'ee/admin/subscriptions/show/constants';
import { useFakeDate } from 'helpers/fake_date';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { sprintf } from '~/locale';
import { license, subscriptionFutureHistory, subscriptionPastHistory } from '../mock_data';
Vue.use(VueApollo);
describe('NoActiveSubscription', () => {
// March 16th, 2020
useFakeDate(2021, 2, 16);
let wrapper;
const findActivateSubscriptionCard = () => wrapper.findComponent(SubscriptionActivationCard);
const findSubscriptionDetailsHistory = () => wrapper.findComponent(SubscriptionDetailsHistory);
const findSubscriptionActivationTitle = () =>
wrapper.findByTestId('subscription-activation-title');
const findSubscriptionFutureLicensesAlert = () =>
wrapper.findByTestId('subscription-future-licenses-alert');
const createComponent = (props, listeners) => {
wrapper = shallowMountExtended(NoActiveSubscription, {
propsData: props,
listeners,
stubs: {
GlSprintf,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('without future subscriptions/licenses', () => {
beforeEach(() => {
createComponent({
subscriptionList: subscriptionPastHistory,
});
});
it('shows a title saying there is no active subscription', () => {
expect(findSubscriptionActivationTitle().text()).toBe(noActiveSubscription);
});
it('it shows the past items', () => {
expect(findSubscriptionDetailsHistory().exists()).toBe(true);
expect(findSubscriptionDetailsHistory().props()).toMatchObject({
subscriptionList: subscriptionPastHistory,
});
});
});
describe('Empty', () => {
beforeEach(() => {
createComponent({
subscriptionList: [],
});
});
it('expect empty', () => {
expect(findSubscriptionDetailsHistory().exists()).toBe(false);
});
});
describe('with future subscriptions/licenses', () => {
beforeEach(() => {
createComponent({
subscriptionList: [...subscriptionPastHistory, ...subscriptionFutureHistory],
});
});
it('shows the upcoming license notification', () => {
expect(findSubscriptionFutureLicensesAlert().exists()).toBe(true);
});
it('shows the upcoming license date in the notification', () => {
// Getting the next future dated license start date
const nextLicenseStartDate = [...subscriptionPastHistory, ...subscriptionFutureHistory]
.filter(({ startsAt }) => isInFuture(new Date(startsAt)))
.sort((a, b) => Date(a) - Date(b))
.pop().startsAt;
const expectedText = sprintf(instanceHasFutureLicenseBanner.message, {
date: nextLicenseStartDate,
});
expect(findSubscriptionFutureLicensesAlert().text()).toBe(expectedText);
});
it('shows the upcoming licenses', () => {
expect(findSubscriptionDetailsHistory().exists()).toBe(true);
expect(findSubscriptionDetailsHistory().props()).toMatchObject({
subscriptionList: [...subscriptionPastHistory, ...subscriptionFutureHistory],
});
});
});
describe('Activation form', () => {
let onSuccess;
beforeEach(() => {
onSuccess = jest.fn();
createComponent(
{
subscriptionList: [],
},
{
[SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT]: onSuccess,
},
);
});
it('shows the subscription activation form', () => {
expect(findActivateSubscriptionCard().exists()).toBe(true);
});
it('passes activation card events', async () => {
findActivateSubscriptionCard().vm.$emit(
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
license.ULTIMATE,
);
await nextTick();
expect(onSuccess).toHaveBeenCalledWith(license.ULTIMATE);
});
});
});
......@@ -35570,6 +35570,12 @@ msgstr ""
msgid "SuperSonics|You do not have an active subscription"
msgstr ""
msgid "SuperSonics|You have a future dated license"
msgstr ""
msgid "SuperSonics|You have added a license that activates on %{date}. Please see the subscription history table below for more details."
msgstr ""
msgid "SuperSonics|You have successfully added a license that activates on %{date}. Please see the subscription history table below for more details."
msgstr ""
......
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