Commit c8d18216 authored by Michael Lunøe's avatar Michael Lunøe

Merge branch '337027-update-subscription-history-table' into 'master'

Show future subscription notification and history table in various places

See merge request gitlab-org/gitlab!77304
parents a26b7400 7fa25e26
...@@ -3,8 +3,6 @@ import { GlAlert, GlButton, GlSprintf } from '@gitlab/ui'; ...@@ -3,8 +3,6 @@ import { GlAlert, GlButton, GlSprintf } from '@gitlab/ui';
import { isInFuture } from '~/lib/utils/datetime/date_calculation_utility'; import { isInFuture } from '~/lib/utils/datetime/date_calculation_utility';
import { sprintf } from '~/locale'; import { sprintf } from '~/locale';
import { import {
activateSubscription,
noActiveSubscription,
subscriptionActivationNotificationText, subscriptionActivationNotificationText,
subscriptionActivationFutureDatedNotificationTitle, subscriptionActivationFutureDatedNotificationTitle,
subscriptionActivationFutureDatedNotificationMessage, subscriptionActivationFutureDatedNotificationMessage,
...@@ -20,10 +18,8 @@ import { ...@@ -20,10 +18,8 @@ import {
import getCurrentLicense from '../graphql/queries/get_current_license.query.graphql'; import getCurrentLicense from '../graphql/queries/get_current_license.query.graphql';
import getPastLicenseHistory from '../graphql/queries/get_past_license_history.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 getFutureLicenseHistory from '../graphql/queries/get_future_license_history.query.graphql';
import SubscriptionActivationCard from './subscription_activation_card.vue';
import SubscriptionBreakdown from './subscription_breakdown.vue'; import SubscriptionBreakdown from './subscription_breakdown.vue';
import SubscriptionPurchaseCard from './subscription_purchase_card.vue'; import NoActiveSubscription from './no_active_subscription.vue';
import SubscriptionTrialCard from './subscription_trial_card.vue';
export default { export default {
name: 'CloudLicenseApp', name: 'CloudLicenseApp',
...@@ -31,15 +27,11 @@ export default { ...@@ -31,15 +27,11 @@ export default {
GlAlert, GlAlert,
GlButton, GlButton,
GlSprintf, GlSprintf,
SubscriptionActivationCard,
SubscriptionBreakdown, SubscriptionBreakdown,
SubscriptionPurchaseCard, NoActiveSubscription,
SubscriptionTrialCard,
}, },
i18n: { i18n: {
activateSubscription,
exportLicenseUsageBtnText, exportLicenseUsageBtnText,
noActiveSubscription,
subscriptionMainTitle, subscriptionMainTitle,
subscriptionHistoryFailedTitle, subscriptionHistoryFailedTitle,
subscriptionHistoryFailedMessage, subscriptionHistoryFailedMessage,
...@@ -176,21 +168,10 @@ export default { ...@@ -176,21 +168,10 @@ export default {
:subscription-list="subscriptionHistory" :subscription-list="subscriptionHistory"
v-on="$options.activationListeners" v-on="$options.activationListeners"
/> />
<div v-else class="row"> <no-active-subscription
<div class="col-12"> v-else
<h3 class="gl-mb-7 gl-mt-6 gl-text-center" data-testid="subscription-activation-title"> :subscription-list="subscriptionHistory"
{{ $options.i18n.noActiveSubscription }} v-on="$options.activationListeners"
</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>
</div> </div>
</template> </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__( ...@@ -158,3 +158,10 @@ export const subscriptionBannerText = s__(
export const subscriptionBannerBlogPostUrl = export const subscriptionBannerBlogPostUrl =
'https://about.gitlab.com/blog/2021/07/20/improved-billing-and-subscription-management/'; 'https://about.gitlab.com/blog/2021/07/20/improved-billing-and-subscription-management/';
export const exportLicenseUsageBtnText = s__('SuperSonics|Export license usage file'); 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 { GlButton, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue from 'vue'; import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import SubscriptionManagementApp from 'ee/admin/subscriptions/show/components/app.vue'; 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 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 { import {
noActiveSubscription,
subscriptionActivationNotificationText, subscriptionActivationNotificationText,
subscriptionActivationFutureDatedNotificationTitle, subscriptionActivationFutureDatedNotificationTitle,
subscriptionHistoryFailedTitle, subscriptionHistoryFailedTitle,
...@@ -35,10 +34,8 @@ describe('SubscriptionManagementApp', () => { ...@@ -35,10 +34,8 @@ describe('SubscriptionManagementApp', () => {
let wrapper; let wrapper;
const findActivateSubscriptionCard = () => wrapper.findComponent(SubscriptionActivationCard);
const findSubscriptionBreakdown = () => wrapper.findComponent(SubscriptionBreakdown); const findSubscriptionBreakdown = () => wrapper.findComponent(SubscriptionBreakdown);
const findSubscriptionActivationTitle = () => const findNoActiveSubscription = () => wrapper.findComponent(NoActiveSubscription);
wrapper.findByTestId('subscription-activation-title');
const findSubscriptionMainTitle = () => wrapper.findByTestId('subscription-main-title'); const findSubscriptionMainTitle = () => wrapper.findByTestId('subscription-main-title');
const findSubscriptionActivationSuccessAlert = () => const findSubscriptionActivationSuccessAlert = () =>
wrapper.findByTestId('subscription-activation-success-alert'); wrapper.findByTestId('subscription-activation-success-alert');
...@@ -77,8 +74,8 @@ describe('SubscriptionManagementApp', () => { ...@@ -77,8 +74,8 @@ describe('SubscriptionManagementApp', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('when failing to fetch subcriptions', () => { describe('when failing to fetch subscriptions', () => {
describe('when failing to fetch history subcriptions', () => { describe('when failing to fetch history subscriptions', () => {
describe.each` describe.each`
currentFails | pastFails | futureFails currentFails | pastFails | futureFails
${true} | ${false} | ${false} ${true} | ${false} | ${false}
...@@ -139,7 +136,6 @@ describe('SubscriptionManagementApp', () => { ...@@ -139,7 +136,6 @@ describe('SubscriptionManagementApp', () => {
}); });
}); });
describe('Subscription Activation Form', () => {
it('shows the main title', () => { it('shows the main title', () => {
currentSubscriptionResolver = jest currentSubscriptionResolver = jest
.fn() .fn()
...@@ -158,26 +154,34 @@ describe('SubscriptionManagementApp', () => { ...@@ -158,26 +154,34 @@ describe('SubscriptionManagementApp', () => {
expect(findSubscriptionMainTitle().text()).toBe(subscriptionMainTitle); expect(findSubscriptionMainTitle().text()).toBe(subscriptionMainTitle);
}); });
describe('Subscription Activation Form', () => {
describe('without an active license', () => { describe('without an active license', () => {
beforeEach(() => { beforeEach(async () => {
currentSubscriptionResolver = jest currentSubscriptionResolver = jest
.fn() .fn()
.mockResolvedValue({ data: { currentLicense: null } }); .mockResolvedValue({ data: { currentLicense: null } });
pastSubscriptionsResolver = jest pastSubscriptionsResolver = jest.fn().mockResolvedValue({
.fn() data: { licenseHistoryEntries: { nodes: subscriptionPastHistory } },
.mockResolvedValue({ data: { licenseHistoryEntries: { nodes: [] } } }); });
futureSubscriptionsResolver = jest futureSubscriptionsResolver = jest.fn().mockResolvedValue({
.fn() data: { subscriptionFutureEntries: { nodes: subscriptionFutureHistory } },
.mockResolvedValue({ data: { subscriptionFutureEntries: { nodes: [] } } }); });
createComponent({}, [ createComponent({ hasActiveLicense: false }, [
currentSubscriptionResolver, currentSubscriptionResolver,
pastSubscriptionsResolver, pastSubscriptionsResolver,
futureSubscriptionsResolver, futureSubscriptionsResolver,
]); ]);
await waitForPromises();
}); });
it('shows a title saying there is no active subscription', () => { it('shows the no active subscription state', () => {
expect(findSubscriptionActivationTitle().text()).toBe(noActiveSubscription); 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', () => { it('queries for the past history', () => {
...@@ -188,10 +192,6 @@ describe('SubscriptionManagementApp', () => { ...@@ -188,10 +192,6 @@ describe('SubscriptionManagementApp', () => {
expect(futureSubscriptionsResolver).toHaveBeenCalledTimes(1); expect(futureSubscriptionsResolver).toHaveBeenCalledTimes(1);
}); });
it('shows the subscription activation form', () => {
expect(findActivateSubscriptionCard().exists()).toBe(true);
});
it('does not show the activation success notification', () => { it('does not show the activation success notification', () => {
expect(findSubscriptionActivationSuccessAlert().exists()).toBe(false); expect(findSubscriptionActivationSuccessAlert().exists()).toBe(false);
}); });
...@@ -202,11 +202,11 @@ describe('SubscriptionManagementApp', () => { ...@@ -202,11 +202,11 @@ describe('SubscriptionManagementApp', () => {
describe('activating the license', () => { describe('activating the license', () => {
it('shows the activation success notification', async () => { it('shows the activation success notification', async () => {
findActivateSubscriptionCard().vm.$emit( findNoActiveSubscription().vm.$emit(
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT, SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
license.ULTIMATE, license.ULTIMATE,
); );
await waitForPromises(); await nextTick();
expect(findSubscriptionActivationSuccessAlert().props('title')).toBe( expect(findSubscriptionActivationSuccessAlert().props('title')).toBe(
subscriptionActivationNotificationText, subscriptionActivationNotificationText,
...@@ -214,10 +214,12 @@ describe('SubscriptionManagementApp', () => { ...@@ -214,10 +214,12 @@ describe('SubscriptionManagementApp', () => {
}); });
it('shows the future dated activation success notification', async () => { it('shows the future dated activation success notification', async () => {
await findActivateSubscriptionCard().vm.$emit( findNoActiveSubscription().vm.$emit(
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT, SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
license.ULTIMATE_FUTURE_DATED, license.ULTIMATE_FUTURE_DATED,
); );
await nextTick();
expect(findSubscriptionActivationSuccessAlert().props('title')).toBe( expect(findSubscriptionActivationSuccessAlert().props('title')).toBe(
subscriptionActivationFutureDatedNotificationTitle, subscriptionActivationFutureDatedNotificationTitle,
); );
...@@ -261,10 +263,12 @@ describe('SubscriptionManagementApp', () => { ...@@ -261,10 +263,12 @@ describe('SubscriptionManagementApp', () => {
}); });
it('shows the activation success notification', async () => { it('shows the activation success notification', async () => {
await findSubscriptionBreakdown().vm.$emit( findSubscriptionBreakdown().vm.$emit(
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT, SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
license.ULTIMATE, license.ULTIMATE,
); );
await nextTick();
expect(findSubscriptionActivationSuccessAlert().props('title')).toBe( expect(findSubscriptionActivationSuccessAlert().props('title')).toBe(
subscriptionActivationNotificationText, subscriptionActivationNotificationText,
); );
...@@ -281,10 +285,12 @@ describe('SubscriptionManagementApp', () => { ...@@ -281,10 +285,12 @@ describe('SubscriptionManagementApp', () => {
}); });
it('calls refetch to update local state', async () => { it('calls refetch to update local state', async () => {
await findSubscriptionBreakdown().vm.$emit( findSubscriptionBreakdown().vm.$emit(
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT, SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
license.ULTIMATE_FUTURE_DATED, license.ULTIMATE_FUTURE_DATED,
); );
await nextTick();
expect(wrapper.vm.$apollo.queries.currentSubscription.refetch).toHaveBeenCalledTimes(1); expect(wrapper.vm.$apollo.queries.currentSubscription.refetch).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$apollo.queries.pastLicenseHistoryEntries.refetch).toHaveBeenCalledTimes( expect(wrapper.vm.$apollo.queries.pastLicenseHistoryEntries.refetch).toHaveBeenCalledTimes(
1, 1,
...@@ -333,7 +339,7 @@ describe('SubscriptionManagementApp', () => { ...@@ -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); 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 "" ...@@ -35570,6 +35570,12 @@ msgstr ""
msgid "SuperSonics|You do not have an active subscription" msgid "SuperSonics|You do not have an active subscription"
msgstr "" 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." msgid "SuperSonics|You have successfully added a license that activates on %{date}. Please see the subscription history table below for more details."
msgstr "" 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