Commit ad7f40bc authored by mlunoe's avatar mlunoe

Feat(SM: Subscription History): add future subscr

Add future subscriptions to the subscription
history table for self-managed users

Changelog: added
EE: true
parent 965fd314
......@@ -11,13 +11,15 @@ import {
subscriptionHistoryFailedTitle,
subscriptionHistoryFailedMessage,
currentSubscriptionsEntryName,
historySubscriptionsEntryName,
pastSubscriptionsEntryName,
futureSubscriptionsEntryName,
subscriptionMainTitle,
exportLicenseUsageBtnText,
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
} from '../constants';
import getCurrentLicense from '../graphql/queries/get_current_license.query.graphql';
import getLicenseHistory from '../graphql/queries/get_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 SubscriptionActivationCard from './subscription_activation_card.vue';
import SubscriptionBreakdown from './subscription_breakdown.vue';
import SubscriptionPurchaseCard from './subscription_purchase_card.vue';
......@@ -63,21 +65,31 @@ export default {
this.subscriptionFetchError = currentSubscriptionsEntryName;
},
},
subscriptionHistory: {
query: getLicenseHistory,
pastLicenseHistoryEntries: {
query: getPastLicenseHistory,
update({ licenseHistoryEntries }) {
return licenseHistoryEntries?.nodes || [];
},
error() {
this.subscriptionFetchError = historySubscriptionsEntryName;
this.subscriptionFetchError = pastSubscriptionsEntryName;
},
},
futureLicenseHistoryEntries: {
query: getFutureLicenseHistory,
update({ subscriptionFutureEntries }) {
return subscriptionFutureEntries?.nodes || [];
},
error() {
this.subscriptionFetchError = futureSubscriptionsEntryName;
},
},
},
data() {
return {
currentSubscription: {},
pastLicenseHistoryEntries: [],
futureLicenseHistoryEntries: [],
activationNotification: null,
subscriptionHistory: [],
subscriptionFetchError: null,
};
},
......@@ -88,6 +100,9 @@ export default {
canShowSubscriptionDetails() {
return this.hasActiveLicense || this.hasValidSubscriptionData;
},
subscriptionHistory() {
return [...this.futureLicenseHistoryEntries, ...this.pastLicenseHistoryEntries];
},
},
created() {
this.$options.activationListeners = {
......
......@@ -64,6 +64,13 @@ export default {
},
{
key: 'activatedAt',
formatter: (v, k, { activatedAt }) => {
if (!activatedAt) {
return '-';
}
return activatedAt;
},
label: subscriptionTable.activatedAt,
tdAttr,
tdClass: this.cellClass,
......@@ -109,11 +116,9 @@ export default {
isCurrentSubscription({ id }) {
return id === this.currentSubscriptionId;
},
rowAttr(item) {
rowAttr() {
return {
'data-testid': this.isCurrentSubscription(item)
? 'subscription-current'
: 'subscription-history-row',
'data-testid': 'subscription-history-row',
};
},
rowClass(item) {
......
......@@ -22,7 +22,8 @@ export const subscriptionHistoryFailedMessage = s__(
'SuperSonics|Your %{subscriptionEntryName} cannot be displayed at the moment. Please refresh the page to try again.',
);
export const currentSubscriptionsEntryName = s__('SuperSonics|current subscription');
export const historySubscriptionsEntryName = s__('SuperSonics|history subscriptions');
export const pastSubscriptionsEntryName = s__('SuperSonics|past subscriptions');
export const futureSubscriptionsEntryName = s__('SuperSonics|future subscriptions');
export const cancelLabel = __('Cancel');
export const activateLabel = s__('AdminUsers|Activate');
......
query getFutureLicenseHistory {
subscriptionFutureEntries {
nodes {
type
plan
name
email
company
usersInLicenseCount
startsAt
expiresAt
}
}
}
import produce from 'immer';
import getCurrentLicense from './queries/get_current_license.query.graphql';
import getLicenseHistory from './queries/get_license_history.query.graphql';
import getPastLicenseHistory from './queries/get_past_license_history.query.graphql';
export const getLicenseFromData = ({ data } = {}) => data?.gitlabSubscriptionActivate?.license;
export const getErrorsAsData = ({ data } = {}) => data?.gitlabSubscriptionActivate?.errors || [];
......@@ -14,12 +14,12 @@ export const updateSubscriptionAppCache = (cache, mutation) => {
draftData.currentLicense = license;
});
cache.writeQuery({ query: getCurrentLicense, data });
const subscriptionsList = cache.readQuery({ query: getLicenseHistory });
const subscriptionListData = produce(subscriptionsList, (draftData) => {
const pastSubscriptions = cache.readQuery({ query: getPastLicenseHistory });
const pastSubscriptionsData = produce(pastSubscriptions, (draftData) => {
draftData.licenseHistoryEntries.nodes = [
license,
...subscriptionsList.licenseHistoryEntries.nodes,
...pastSubscriptions.licenseHistoryEntries.nodes,
];
});
cache.writeQuery({ query: getLicenseHistory, data: subscriptionListData });
cache.writeQuery({ query: getPastLicenseHistory, data: pastSubscriptionsData });
};
......@@ -12,18 +12,20 @@ import {
subscriptionHistoryFailedTitle,
subscriptionHistoryFailedMessage,
currentSubscriptionsEntryName,
historySubscriptionsEntryName,
pastSubscriptionsEntryName,
futureSubscriptionsEntryName,
subscriptionMainTitle,
SUBSCRIPTION_ACTIVATION_SUCCESS_EVENT,
} from 'ee/admin/subscriptions/show/constants';
import getCurrentLicense from 'ee/admin/subscriptions/show/graphql/queries/get_current_license.query.graphql';
import getLicenseHistory from 'ee/admin/subscriptions/show/graphql/queries/get_license_history.query.graphql';
import getPastLicenseHistory from 'ee/admin/subscriptions/show/graphql/queries/get_past_license_history.query.graphql';
import getFutureLicenseHistory from 'ee/admin/subscriptions/show/graphql/queries/get_future_license_history.query.graphql';
import waitForPromises from 'helpers/wait_for_promises';
import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { sprintf } from '~/locale';
import { license, subscriptionHistory } from '../mock_data';
import { license, subscriptionPastHistory, subscriptionFutureHistory } from '../mock_data';
Vue.use(VueApollo);
......@@ -45,12 +47,14 @@ describe('SubscriptionManagementApp', () => {
const findExportLicenseUsageFileLink = () => wrapper.findComponent(GlButton);
let currentSubscriptionResolver;
let subscriptionHistoryResolver;
const createMockApolloProvider = ([subscriptionResolver, historyResolver]) => {
let pastSubscriptionsResolver;
let futureSubscriptionsResolver;
const createMockApolloProvider = ([currentResolver, pastResolver, futureResolver]) => {
Vue.use(VueApollo);
return createMockApollo([
[getCurrentLicense, subscriptionResolver],
[getLicenseHistory, historyResolver],
[getCurrentLicense, currentResolver],
[getPastLicenseHistory, pastResolver],
[getFutureLicenseHistory, futureResolver],
]);
};
......@@ -76,34 +80,54 @@ describe('SubscriptionManagementApp', () => {
describe('when failing to fetch subcriptions', () => {
describe('when failing to fetch history subcriptions', () => {
describe.each`
currentFails | historyFails
${true} | ${false}
${false} | ${true}
${true} | ${true}
currentFails | pastFails | futureFails
${true} | ${false} | ${false}
${false} | ${true} | ${false}
${false} | ${false} | ${true}
${true} | ${true} | ${false}
${false} | ${true} | ${true}
${true} | ${false} | ${true}
${true} | ${true} | ${true}
`(
'with current subscription failing to fetch=$currentFails and history subscriptions failing to fetch=$historyFails',
({ currentFails, historyFails }) => {
'with current subscription fetch failing: currentFails=$currentFails, pastFails=$pastFails, and futureFails=$futureFails',
({ currentFails, pastFails, futureFails }) => {
const error = new Error('Network error!');
beforeEach(async () => {
currentSubscriptionResolver = currentFails
? jest.fn().mockRejectedValue({ error })
: jest.fn().mockResolvedValue({ data: { currentLicense: license.ULTIMATE } });
subscriptionHistoryResolver = historyFails
pastSubscriptionsResolver = pastFails
? jest.fn().mockRejectedValue({ error })
: jest.fn().mockResolvedValue({
data: { licenseHistoryEntries: { nodes: subscriptionHistory } },
data: { licenseHistoryEntries: { nodes: subscriptionPastHistory } },
});
futureSubscriptionsResolver = futureFails
? jest.fn().mockRejectedValue({ error })
: jest.fn().mockResolvedValue({
data: { subscriptionFutureEntries: { nodes: subscriptionFutureHistory } },
});
createComponent({}, [currentSubscriptionResolver, subscriptionHistoryResolver]);
createComponent({}, [
currentSubscriptionResolver,
pastSubscriptionsResolver,
futureSubscriptionsResolver,
]);
await waitForPromises();
});
it('renders the error alert', () => {
const alert = findSubscriptionFetchErrorAlert();
const subscriptionEntryName = historyFails
? historySubscriptionsEntryName
: currentSubscriptionsEntryName;
let subscriptionEntryName;
if (currentFails) {
subscriptionEntryName = currentSubscriptionsEntryName;
}
if (pastFails) {
subscriptionEntryName = pastSubscriptionsEntryName;
}
if (futureFails) {
subscriptionEntryName = futureSubscriptionsEntryName;
}
expect(alert.exists()).toBe(true);
expect(alert.props('title')).toBe(subscriptionHistoryFailedTitle);
expect(alert.text().replace(/\s+/g, ' ')).toBe(
......@@ -120,10 +144,17 @@ describe('SubscriptionManagementApp', () => {
currentSubscriptionResolver = jest
.fn()
.mockResolvedValue({ data: { currentLicense: license.ULTIMATE } });
subscriptionHistoryResolver = jest
.fn()
.mockResolvedValue({ data: { licenseHistoryEntries: { nodes: subscriptionHistory } } });
createComponent({}, [currentSubscriptionResolver, subscriptionHistoryResolver]);
pastSubscriptionsResolver = jest.fn().mockResolvedValue({
data: { licenseHistoryEntries: { nodes: subscriptionPastHistory } },
});
futureSubscriptionsResolver = jest.fn().mockResolvedValue({
data: { subscriptionFutureEntries: { nodes: subscriptionFutureHistory } },
});
createComponent({}, [
currentSubscriptionResolver,
pastSubscriptionsResolver,
futureSubscriptionsResolver,
]);
expect(findSubscriptionMainTitle().text()).toBe(subscriptionMainTitle);
});
......@@ -132,17 +163,29 @@ describe('SubscriptionManagementApp', () => {
currentSubscriptionResolver = jest
.fn()
.mockResolvedValue({ data: { currentLicense: null } });
subscriptionHistoryResolver = jest
pastSubscriptionsResolver = jest
.fn()
.mockResolvedValue({ data: { licenseHistoryEntries: { nodes: [] } } });
createComponent({}, [currentSubscriptionResolver, subscriptionHistoryResolver]);
futureSubscriptionsResolver = jest
.fn()
.mockResolvedValue({ data: { subscriptionFutureEntries: { nodes: [] } } });
createComponent({}, [
currentSubscriptionResolver,
pastSubscriptionsResolver,
futureSubscriptionsResolver,
]);
});
it('shows a title saying there is no active subscription', () => {
expect(findSubscriptionActivationTitle().text()).toBe(noActiveSubscription);
});
it('queries for the current history', () => {
expect(subscriptionHistoryResolver).toHaveBeenCalledTimes(1);
it('queries for the past history', () => {
expect(pastSubscriptionsResolver).toHaveBeenCalledTimes(1);
});
it('queries for the future history', () => {
expect(futureSubscriptionsResolver).toHaveBeenCalledTimes(1);
});
it('shows the subscription activation form', () => {
......@@ -185,12 +228,16 @@ describe('SubscriptionManagementApp', () => {
currentSubscriptionResolver = jest
.fn()
.mockResolvedValue({ data: { currentLicense: license.ULTIMATE } });
subscriptionHistoryResolver = jest
.fn()
.mockResolvedValue({ data: { licenseHistoryEntries: { nodes: subscriptionHistory } } });
pastSubscriptionsResolver = jest.fn().mockResolvedValue({
data: { licenseHistoryEntries: { nodes: subscriptionPastHistory } },
});
futureSubscriptionsResolver = jest.fn().mockResolvedValue({
data: { subscriptionFutureEntries: { nodes: subscriptionFutureHistory } },
});
createComponent({ hasActiveLicense: false }, [
currentSubscriptionResolver,
subscriptionHistoryResolver,
pastSubscriptionsResolver,
futureSubscriptionsResolver,
]);
await waitForPromises();
});
......@@ -198,7 +245,7 @@ describe('SubscriptionManagementApp', () => {
it('passes the correct data to the subscription breakdown', () => {
expect(findSubscriptionBreakdown().props()).toMatchObject({
subscription: license.ULTIMATE,
subscriptionList: subscriptionHistory,
subscriptionList: [...subscriptionFutureHistory, ...subscriptionPastHistory],
});
});
......@@ -228,12 +275,16 @@ describe('SubscriptionManagementApp', () => {
currentSubscriptionResolver = jest
.fn()
.mockResolvedValue({ data: { currentLicense: license.ULTIMATE } });
subscriptionHistoryResolver = jest
.fn()
.mockResolvedValue({ data: { licenseHistoryEntries: { nodes: subscriptionHistory } } });
pastSubscriptionsResolver = jest.fn().mockResolvedValue({
data: { licenseHistoryEntries: { nodes: subscriptionPastHistory } },
});
futureSubscriptionsResolver = jest.fn().mockResolvedValue({
data: { subscriptionFutureEntries: { nodes: subscriptionFutureHistory } },
});
createComponent({ hasActiveLicense: true }, [
currentSubscriptionResolver,
subscriptionHistoryResolver,
pastSubscriptionsResolver,
futureSubscriptionsResolver,
]);
await waitForPromises();
});
......@@ -242,14 +293,18 @@ describe('SubscriptionManagementApp', () => {
expect(currentSubscriptionResolver).toHaveBeenCalledTimes(1);
});
it('queries for the current history', () => {
expect(subscriptionHistoryResolver).toHaveBeenCalledTimes(1);
it('queries for the past history', () => {
expect(pastSubscriptionsResolver).toHaveBeenCalledTimes(1);
});
it('queries for the future history', () => {
expect(futureSubscriptionsResolver).toHaveBeenCalledTimes(1);
});
it('passes the correct data to the subscription breakdown', () => {
expect(findSubscriptionBreakdown().props()).toMatchObject({
subscription: license.ULTIMATE,
subscriptionList: subscriptionHistory,
subscriptionList: [...subscriptionFutureHistory, ...subscriptionPastHistory],
});
});
......
......@@ -27,7 +27,7 @@ import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisse
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import { license, subscriptionHistory } from '../mock_data';
import { license, subscriptionPastHistory, subscriptionFutureHistory } from '../mock_data';
describe('Subscription Breakdown', () => {
let axiosMock;
......@@ -35,7 +35,7 @@ describe('Subscription Breakdown', () => {
let glModalDirective;
let userCalloutDismissSpy;
const [, licenseFile] = subscriptionHistory;
const [, licenseFile] = subscriptionPastHistory;
const congratulationSvgPath = '/path/to/svg';
const connectivityHelpURL = 'connectivity/help/url';
const customersPortalUrl = 'customers.dot';
......@@ -88,7 +88,7 @@ describe('Subscription Breakdown', () => {
},
propsData: {
subscription: license.ULTIMATE,
subscriptionList: subscriptionHistory,
subscriptionList: [...subscriptionFutureHistory, ...subscriptionPastHistory],
...props,
},
stubs: {
......@@ -398,7 +398,10 @@ describe('Subscription Breakdown', () => {
it('provides the correct props to the subscription history component', () => {
expect(findDetailsHistory().props('currentSubscriptionId')).toBe(license.ULTIMATE.id);
expect(findDetailsHistory().props('subscriptionList')).toBe(subscriptionHistory);
expect(findDetailsHistory().props('subscriptionList')).toMatchObject([
...subscriptionFutureHistory,
...subscriptionPastHistory,
]);
});
});
......
import { GlBadge, GlIcon, GlTooltip } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import SubscriptionDetailsHistory from 'ee/admin/subscriptions/show/components/subscription_details_history.vue';
import { detailsLabels, cloudLicenseText } from 'ee/admin/subscriptions/show/constants';
import {
detailsLabels,
cloudLicenseText,
licenseFileText,
subscriptionTypes,
} from 'ee/admin/subscriptions/show/constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { license, subscriptionHistory } from '../mock_data';
import { license, subscriptionFutureHistory, subscriptionPastHistory } from '../mock_data';
const subscriptionList = [...subscriptionFutureHistory, ...subscriptionPastHistory];
const currentSubscriptionIndex = subscriptionFutureHistory.length;
describe('Subscription Details History', () => {
let wrapper;
const findCurrentRow = () => wrapper.findByTestId('subscription-current');
const findTableRows = () => wrapper.findAllByTestId('subscription-history-row');
const findCurrentRow = () => findTableRows().at(currentSubscriptionIndex);
const cellFinder = (row) => (testId) => extendedWrapper(row).findByTestId(testId);
const containsABadge = (row) => row.findComponent(GlBadge).exists();
const containsATooltip = (row) => row.findComponent(GlTooltip).exists();
......@@ -20,7 +29,7 @@ describe('Subscription Details History', () => {
mount(SubscriptionDetailsHistory, {
propsData: {
currentSubscriptionId: license.ULTIMATE.id,
subscriptionList: subscriptionHistory,
subscriptionList,
...props,
},
}),
......@@ -41,12 +50,12 @@ describe('Subscription Details History', () => {
});
it('has the correct number of subscription rows', () => {
expect(findTableRows()).toHaveLength(1);
expect(findTableRows()).toHaveLength(subscriptionList.length);
});
it('has the correct license type', () => {
expect(findCurrentRow().text()).toContain(cloudLicenseText);
expect(findTableRows().at(0).text()).toContain('License file');
expect(findTableRows().at(-1).text()).toContain('License file');
});
it('has a badge for the license type', () => {
......@@ -73,49 +82,53 @@ describe('Subscription Details History', () => {
expect(findTableRows().at(0).classes('gl-text-blue-500')).toBe(false);
});
describe('cell data', () => {
describe.each(Object.entries(subscriptionList))('cell data index=%#', (index, subscription) => {
let findCellByTestid;
beforeEach(() => {
createComponent();
findCellByTestid = cellFinder(findCurrentRow());
findCellByTestid = cellFinder(findTableRows().at(index));
});
it.each`
testId | key
${'starts-at'} | ${'startsAt'}
${'starts-at'} | ${'startsAt'}
${'expires-at'} | ${'expiresAt'}
${'users-in-license-count'} | ${'usersInLicenseCount'}
`('displays the correct value for the $testId cell', ({ testId, key }) => {
const cellTestId = `subscription-cell-${testId}`;
expect(findCellByTestid(cellTestId).text()).toBe(subscriptionHistory[0][key]);
const value = subscription[key] || '-';
expect(findCellByTestid(cellTestId).text()).toBe(value);
});
it('displays the name field with tooltip', () => {
const cellTestId = 'subscription-cell-name';
const text = findCellByTestid(cellTestId).text();
expect(text).toContain(subscriptionHistory[0].name);
expect(text).toContain(`(${subscriptionHistory[0].company})`);
expect(text).toContain(subscriptionHistory[0].email);
expect(text).toContain(subscription.name);
expect(text).toContain(`(${subscription.company})`);
expect(text).toContain(subscription.email);
});
it('displays sr-only element for screen readers', () => {
const testId = 'subscription-history-sr-only';
const text = findCellByTestid(testId).text();
expect(text).not.toContain(subscriptionHistory[0].name);
expect(text).toContain(`(${detailsLabels.company}: ${subscriptionHistory[0].company})`);
expect(text).toContain(`${detailsLabels.email}: ${subscriptionHistory[0].email}`);
expect(text).not.toContain(subscription.name);
expect(text).toContain(`(${detailsLabels.company}: ${subscription.company})`);
expect(text).toContain(`${detailsLabels.email}: ${subscription.email}`);
});
it('displays the correct value for the type cell', () => {
const cellTestId = `subscription-cell-type`;
expect(findCellByTestid(cellTestId).text()).toBe(cloudLicenseText);
const type =
subscription.type === subscriptionTypes.LICENSE_FILE ? licenseFileText : cloudLicenseText;
expect(findCellByTestid(cellTestId).text()).toBe(type);
});
it('displays the correct value for the plan cell', () => {
const cellTestId = `subscription-cell-plan`;
expect(findCellByTestid(cellTestId).text()).toBe('Ultimate');
expect(findCellByTestid(cellTestId).text()).toBe(
capitalizeFirstCharacter(subscription.plan),
);
});
});
});
......
......@@ -35,7 +35,7 @@ export const license = {
},
};
export const subscriptionHistory = [
export const subscriptionPastHistory = [
{
activatedAt: '2022-03-16',
company: 'ACME Corp',
......@@ -62,6 +62,29 @@ export const subscriptionHistory = [
},
];
export const subscriptionFutureHistory = [
{
company: 'ACME Corp',
email: 'user@acmecorp.com',
expiresAt: '2023-03-16',
name: 'Jane Doe',
plan: 'ultimate',
startsAt: '2022-03-11',
type: subscriptionTypes.CLOUD,
usersInLicenseCount: '15',
},
{
company: 'ACME Corp',
email: 'user@acmecorp.com',
expiresAt: '2022-03-16',
name: 'Jane Doe',
plan: 'ultimate',
startsAt: '2021-03-16',
type: subscriptionTypes.CLOUD,
usersInLicenseCount: '10',
},
];
export const activateLicenseMutationResponse = {
FAILURE: [
{
......
......@@ -34954,7 +34954,10 @@ msgstr ""
msgid "SuperSonics|current subscription"
msgstr ""
msgid "SuperSonics|history subscriptions"
msgid "SuperSonics|future subscriptions"
msgstr ""
msgid "SuperSonics|past subscriptions"
msgstr ""
msgid "Support"
......
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