Commit e9eb7967 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch '8838-adapt-subscriptions-page-for-free-plans-and-trials' into 'master'

Resolve "Adapt subscriptions page for free plans and trials"

Closes #8838

See merge request gitlab-org/gitlab-ee!8838
parents 71381976 9feeceba
......@@ -3,7 +3,12 @@ import _ from 'underscore';
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import SubscriptionTableRow from './subscription_table_row.vue';
import { CUSTOMER_PORTAL_URL } from '../constants';
import {
CUSTOMER_PORTAL_URL,
TABLE_TYPE_DEFAULT,
TABLE_TYPE_FREE,
TABLE_TYPE_TRIAL,
} from '../constants';
import { s__, sprintf } from '~/locale';
export default {
......@@ -13,12 +18,12 @@ export default {
GlLoadingIcon,
},
computed: {
...mapState('subscription', ['isLoading', 'hasError', 'plan', 'rows', 'endpoint']),
...mapState('subscription', ['isLoading', 'hasError', 'plan', 'tables', 'endpoint']),
...mapGetters('subscription', ['isFreePlan']),
subscriptionHeader() {
let suffix = this.isFreePlan ? '' : s__('SubscriptionTable|subscription');
let suffix = '';
if (!this.isFreePlan && this.plan.trial) {
suffix += ` - ${s__('SubscriptionTable|Trial')}`;
suffix = `${s__('SubscriptionTable|Trial')}`;
}
return sprintf(s__('SubscriptionTable|GitLab.com %{planName} %{suffix}'), {
planName: this.isFreePlan ? s__('SubscriptionTable|Free') : _.escape(this.plan.name),
......@@ -28,6 +33,17 @@ export default {
actionButtonText() {
return this.isFreePlan ? s__('SubscriptionTable|Upgrade') : s__('SubscriptionTable|Manage');
},
visibleRows() {
let tableKey = TABLE_TYPE_DEFAULT;
if (this.plan.code === null) {
tableKey = TABLE_TYPE_FREE;
} else if (this.plan.trial) {
tableKey = TABLE_TYPE_TRIAL;
}
return this.tables[tableKey].rows;
},
},
mounted() {
this.fetchSubscription();
......@@ -60,7 +76,7 @@ export default {
</div>
<div class="card-body flex-grid d-flex flex-column flex-sm-row flex-md-row flex-lg-column">
<subscription-table-row
v-for="(row, i) in rows"
v-for="(row, i) in visibleRows"
:key="`subscription-rows-${i}`"
:header="row.header"
:columns="row.columns"
......
......@@ -56,7 +56,11 @@ export default {
</span>
</div>
<template v-for="(col, i) in columns">
<div :key="`subscription-col-${i}`" class="grid-cell" :class="[col.hidden ? 'no-value' : '']">
<div
:key="`subscription-col-${i}`"
class="grid-cell"
:class="[col.hideContent ? 'no-value' : '']"
>
<span class="property-label"> {{ col.label }} </span>
<popover v-if="col.popover" :options="getPopoverOptions(col)" />
<p
......
export const USAGE_ROW_INDEX = 0;
export const BILLING_ROW_INDEX = 1;
export const CUSTOMER_PORTAL_URL = 'https://customers.gitlab.com/subscriptions';
export const TABLE_TYPE_DEFAULT = 'default';
export const TABLE_TYPE_FREE = 'free';
export const TABLE_TYPE_TRIAL = 'trial';
import Vue from 'vue';
import { TABLE_TYPE_DEFAULT, TABLE_TYPE_FREE, TABLE_TYPE_TRIAL } from '../../../constants';
import * as types from './mutation_types';
import { USAGE_ROW_INDEX, BILLING_ROW_INDEX } from '../../../constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default {
......@@ -16,22 +16,22 @@ export default {
[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload) {
const data = convertObjectPropsToCamelCase(payload, { deep: true });
const { plan, usage, billing } = data;
let tableKey = TABLE_TYPE_DEFAULT;
state.plan = plan;
/*
* Update column values for billing and usage row.
* We iterate over the rows within the state
* and update only the column's value property in the state
* with the data we received from the API for the given column
*/
[USAGE_ROW_INDEX, BILLING_ROW_INDEX].forEach(rowIdx => {
const currentRow = state.rows[rowIdx];
currentRow.columns.forEach(currentCol => {
if (rowIdx === USAGE_ROW_INDEX) {
Vue.set(currentCol, 'value', usage[currentCol.id]);
} else if (rowIdx === BILLING_ROW_INDEX) {
Vue.set(currentCol, 'value', billing[currentCol.id]);
if (state.plan.code === null) {
tableKey = TABLE_TYPE_FREE;
} else if (state.plan.trial) {
tableKey = TABLE_TYPE_TRIAL;
}
state.tables[tableKey].rows.forEach(row => {
row.columns.forEach(col => {
if (Object.prototype.hasOwnProperty.call(usage, col.id)) {
Vue.set(col, 'value', usage[col.id]);
} else if (Object.prototype.hasOwnProperty.call(billing, col.id)) {
Vue.set(col, 'value', billing[col.id]);
}
});
});
......
......@@ -9,95 +9,161 @@ export default () => ({
name: null,
trial: false,
},
rows: [
{
header: {
icon: 'monitor',
title: s__('SubscriptionTable|Usage'),
},
columns: [
tables: {
free: {
rows: [
{
id: 'seatsInSubscription',
label: s__('SubscriptionTable|Seats in subscription'),
value: null,
colClass: 'number',
},
{
id: 'seatsInUse',
label: s__('SubscriptionTable|Seats currently in use'),
value: null,
colClass: 'number',
popover: {
content: s__(`SubscriptionTable|Usage count is performed once a day at 12:00 PM.`),
},
},
{
id: 'maxSeatsUsed',
label: s__('SubscriptionTable|Max seats used'),
value: null,
colClass: 'number',
popover: {
content: s__(
'SubscriptionTable|This is the maximum number of users that have existed at the same time since this subscription started.',
),
header: {
icon: 'monitor',
title: s__('SubscriptionTable|Usage'),
},
columns: [
{
id: 'seatsInUse',
label: s__('SubscriptionTable|Seats currently in use'),
value: null,
colClass: 'number',
popover: {
content: s__(
'SubscriptionTable|This is the number of seats you will be required to purchase if you update to a paid plan.',
),
},
},
{
id: 'subscriptionStartDate',
label: s__('SubscriptionTable|Subscription start date'),
value: null,
isDate: true,
},
],
},
],
},
trial: {
rows: [
{
id: 'seatsOwed',
label: s__('SubscriptionTable|Seats owed'),
value: null,
colClass: 'number',
popover: {
content: s__(
'SubscriptionTable|GitLab allows you to continue using your subscription even if you exceed the number of seats you purchased. You will be required to pay for these seats upon renewal.',
),
header: {
icon: 'monitor',
title: s__('SubscriptionTable|Usage'),
},
columns: [
{
id: 'seatsInUse',
label: s__('SubscriptionTable|Seats currently in use'),
value: null,
colClass: 'number',
popover: {
content: s__('SubscriptionTable|Usage count is performed once a day at 12:00 PM.'),
},
},
{
id: 'subscriptionStartDate',
label: s__('SubscriptionTable|Trial start date'),
value: null,
isDate: true,
},
{
id: 'subscriptionEndDate',
label: s__('SubscriptionTable|Trial end date'),
value: null,
isDate: true,
},
],
},
],
},
{
header: {
icon: 'calendar',
title: s__('SubscriptionTable|Billing'),
},
columns: [
{
id: 'subscriptionStartDate',
label: s__('SubscriptionTable|Subscription start date'),
value: null,
isDate: true,
},
default: {
rows: [
{
id: 'subscriptionEndDate',
label: s__('SubscriptionTable|Subscription end date'),
value: null,
isDate: true,
},
{
id: 'lastInvoice',
label: s__('SubscriptionTable|Last invoice'),
value: null,
isDate: true,
popover: {
content: s__(
'SubscriptionTable|This is the last time the GitLab.com team was in contact with you to settle any outstanding balances.',
),
header: {
icon: 'monitor',
title: s__('SubscriptionTable|Usage'),
},
hidden: true,
columns: [
{
id: 'seatsInSubscription',
label: s__('SubscriptionTable|Seats in subscription'),
value: null,
colClass: 'number',
},
{
id: 'seatsInUse',
label: s__('SubscriptionTable|Seats currently in use'),
value: null,
colClass: 'number',
popover: {
content: s__('SubscriptionTable|Usage count is performed once a day at 12:00 PM.'),
},
},
{
id: 'maxSeatsUsed',
label: s__('SubscriptionTable|Max seats used'),
value: null,
colClass: 'number',
popover: {
content: s__(
'SubscriptionTable|This is the maximum number of users that have existed at the same time since this subscription started.',
),
},
},
{
id: 'seatsOwed',
label: s__('SubscriptionTable|Seats owed'),
value: null,
colClass: 'number',
popover: {
content: s__(
'SubscriptionTable|GitLab allows you to continue using your subscription even if you exceed the number of seats you purchased. You will be required to pay for these seats upon renewal.',
),
},
},
],
},
{
id: 'nextInvoice',
label: s__('SubscriptionTable|Next invoice'),
value: null,
isDate: true,
popover: {
content: s__(
'SubscriptionTable|This is the next date when the GitLab.com team is scheduled to get in contact with you to settle any outstanding balances.',
),
header: {
icon: 'calendar',
title: s__('SubscriptionTable|Billing'),
},
hidden: true,
columns: [
{
id: 'subscriptionStartDate',
label: s__('SubscriptionTable|Subscription start date'),
value: null,
isDate: true,
},
{
id: 'subscriptionEndDate',
label: s__('SubscriptionTable|Subscription end date'),
value: null,
isDate: true,
},
{
id: 'lastInvoice',
label: s__('SubscriptionTable|Last invoice'),
value: null,
isDate: true,
popover: {
content: s__(
'SubscriptionTable|This is the last time the GitLab.com team was in contact with you to settle any outstanding balances.',
),
},
hideContent: true, // temporarily display a blank cell (as we don't have content yet)
},
{
id: 'nextInvoice',
label: s__('SubscriptionTable|Next invoice'),
value: null,
isDate: true,
popover: {
content: s__(
'SubscriptionTable|This is the next date when the GitLab.com team is scheduled to get in contact with you to settle any outstanding balances.',
),
},
hideContent: true, // temporarily display a blank cell (as we don't have content yet)
},
],
},
],
},
],
},
});
---
title: Adapt subscriptions page for free plans and trials
merge_request: 8838
author:
type: other
......@@ -37,10 +37,17 @@ describe('Subscription Table Row', () => {
vm = mountComponent(Component, props);
});
it(`should render one header cell and ${columns.length} columns in total`, () => {
it(`should render one header cell and ${columns.length} visible columns in total`, () => {
expect(vm.$el.querySelectorAll('.grid-cell')).toHaveLength(columns.length + 1);
});
it(`should not render a hidden column`, () => {
const hiddenColIdx = columns.find(c => !c.display);
const hiddenCol = vm.$el.querySelectorAll('.grid-cell:not(.header-cell)')[hiddenColIdx];
expect(hiddenCol).toBe(undefined);
});
it('should render a title in the header cell', () => {
expect(vm.$el.querySelector('.header-cell').textContent).toContain(props.header.title);
});
......
......@@ -2,8 +2,8 @@ import Vue from 'vue';
import component from 'ee/billings/components/subscription_table.vue';
import createStore from 'ee/billings/stores';
import * as types from 'ee/billings/stores/modules/subscription/mutation_types';
import mockDataSubscription from 'ee/billings/stores/modules/subscription/mock_data_subscription.json';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import mockDataSubscription from '../mock_data';
import { resetStore } from '../helpers';
describe('Subscription Table', () => {
......@@ -41,14 +41,17 @@ describe('Subscription Table', () => {
beforeEach(done => {
vm.$store.state.subscription.namespaceId = namespaceId;
vm.$store.commit(`subscription/${types.RECEIVE_SUBSCRIPTION_SUCCESS}`, mockDataSubscription);
vm.$store.commit(
`subscription/${types.RECEIVE_SUBSCRIPTION_SUCCESS}`,
mockDataSubscription.gold,
);
vm.$store.state.subscription.isLoading = false;
vm.$nextTick(done);
});
it('should render the card title "GitLab.com Gold subscription"', () => {
it('should render the card title "GitLab.com Gold"', () => {
expect(vm.$el.querySelector('.js-subscription-header strong').textContent.trim()).toBe(
'GitLab.com Gold subscription',
'GitLab.com Gold',
);
});
......
export default {
gold: {
plan: {
name: 'Gold',
code: 'gold',
trial: false,
},
usage: {
seats_in_subscription: 100,
seats_in_use: 98,
max_seats_used: 104,
seats_owed: 4,
},
billing: {
subscription_start_date: '2018-07-11',
subscription_end_date: '2019-07-11',
last_invoice: '2018-09-01',
next_invoice: '2018-10-01',
},
},
free: {
plan: {
name: null,
code: null,
trial: null,
},
usage: {
seats_in_subscription: 0,
seats_in_use: 0,
max_seats_used: 5,
seats_owed: 0,
},
billing: {
subscription_start_date: '2018-10-30',
subscription_end_date: null,
trial_ends_on: null,
},
},
trial: {
plan: {
name: 'Gold',
code: 'gold',
trial: true,
},
usage: {
seats_in_subscription: 100,
seats_in_use: 1,
max_seats_used: 0,
seats_owed: 0,
},
billing: {
subscription_start_date: '2018-12-13',
subscription_end_date: '2019-12-13',
trial_ends_on: '2019-12-13',
},
},
};
......@@ -6,7 +6,7 @@ import state from 'ee/billings/stores/modules/subscription/state';
import * as types from 'ee/billings/stores/modules/subscription/mutation_types';
import * as actions from 'ee/billings/stores/modules/subscription/actions';
import mockDataSubscription from './data/mock_data_subscription.json';
import mockDataSubscription from '../../../mock_data';
describe('subscription actions', () => {
let mockedState;
......@@ -51,7 +51,7 @@ describe('subscription actions', () => {
beforeEach(() => {
mock
.onGet(/\/api\/v4\/namespaces\/\d+\/gitlab_subscription(.*)$/)
.replyOnce(200, mockDataSubscription);
.replyOnce(200, mockDataSubscription.gold);
});
it('should dispatch the request and success actions', done => {
......@@ -64,7 +64,7 @@ describe('subscription actions', () => {
{ type: 'requestSubscription' },
{
type: 'receiveSubscriptionSuccess',
payload: mockDataSubscription,
payload: mockDataSubscription.gold,
},
],
done,
......@@ -107,12 +107,12 @@ describe('subscription actions', () => {
it('should commit the success mutation', done => {
testAction(
actions.receiveSubscriptionSuccess,
mockDataSubscription,
mockDataSubscription.gold,
mockedState,
[
{
type: types.RECEIVE_SUBSCRIPTION_SUCCESS,
payload: mockDataSubscription,
payload: mockDataSubscription.gold,
},
],
[],
......
{
"plan": {
"name": "Gold",
"code": "gold",
"trial": false
},
"usage": {
"seats_in_subscription": 100,
"seats_in_use": 98,
"max_seats_used": 104,
"seats_owed": 4
},
"billing": {
"subscription_start_date": "2018-07-11",
"subscription_end_date": "2019-07-11",
"last_invoice": "2018-09-01",
"next_invoice": "2018-10-01"
}
}
import createState from 'ee/billings/stores/modules/subscription/state';
import * as types from 'ee/billings/stores/modules/subscription/mutation_types';
import mutations from 'ee/billings/stores/modules/subscription/mutations';
import { USAGE_ROW_INDEX, BILLING_ROW_INDEX } from 'ee/billings/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import mockData from './data/mock_data_subscription.json';
import mockData from '../../../mock_data';
describe('subscription module mutations', () => {
describe('SET_PNAMESPACE_ID', () => {
describe('SET_NAMESPACE_ID', () => {
it('should set "namespaceId" to "1"', () => {
const state = createState();
const namespaceId = '1';
......@@ -34,35 +33,95 @@ describe('subscription module mutations', () => {
let payload;
let state;
beforeEach(() => {
payload = mockData;
state = createState();
mutations[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload);
});
describe('Gold subscription', () => {
beforeEach(() => {
state = createState();
payload = mockData.gold;
mutations[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload);
});
it('should set "isLoading" to "false"', () => {
expect(state.isLoading).toBeFalsy();
});
it('should set "isLoading" to "false"', () => {
expect(state.isLoading).toBeFalsy();
});
it('should set "plan" attributes', () => {
expect(state.plan.code).toBe(payload.plan.code);
expect(state.plan.name).toBe(payload.plan.name);
expect(state.plan.trial).toBe(payload.plan.trial);
});
it('should set "plan" attributes', () => {
expect(state.plan.code).toBe(payload.plan.code);
expect(state.plan.name).toBe(payload.plan.name);
expect(state.plan.trial).toBe(payload.plan.trial);
it('should set the column values on the "Usage" row', () => {
const usageRow = state.tables.default.rows[0];
const data = convertObjectPropsToCamelCase(payload, { deep: true });
usageRow.columns.forEach(column => {
expect(column.value).toBe(data.usage[column.id]);
});
});
it('should set the column values on the "Billing" row', () => {
const billingRow = state.tables.default.rows[1];
const data = convertObjectPropsToCamelCase(payload, { deep: true });
billingRow.columns.forEach(column => {
expect(column.value).toBe(data.billing[column.id]);
});
});
});
it('should set the column values on the "Usage" row', () => {
const usageRow = state.rows[USAGE_ROW_INDEX];
const data = convertObjectPropsToCamelCase(payload, { deep: true });
usageRow.columns.forEach(column => {
expect(column.value).toBe(data.usage[column.id]);
describe('Free plan', () => {
beforeEach(() => {
state = createState();
payload = mockData.free;
mutations[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload);
});
it('should set "isLoading" to "false"', () => {
expect(state.isLoading).toBeFalsy();
});
it('should set "plan" attributes', () => {
expect(state.plan.code).toBe(payload.plan.code);
expect(state.plan.name).toBe(payload.plan.name);
expect(state.plan.trial).toBe(payload.plan.trial);
});
it('should populate "subscriptionStartDate" from "billings row" correctly', () => {
const usageRow = state.tables.free.rows[0];
const data = convertObjectPropsToCamelCase(payload, { deep: true });
usageRow.columns.forEach(column => {
if (column.id === 'subscriptionStartDate') {
expect(column.value).toBe(data.billing.subscriptionStartDate);
}
});
});
});
it('should set the column values on the "Billing" row', () => {
const billingow = state.rows[BILLING_ROW_INDEX];
const data = convertObjectPropsToCamelCase(payload, { deep: true });
billingow.columns.forEach(column => {
expect(column.value).toBe(data.billing[column.id]);
describe('Gold trial', () => {
beforeEach(() => {
state = createState();
payload = mockData.trial;
mutations[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload);
});
it('should set "isLoading" to "false"', () => {
expect(state.isLoading).toBeFalsy();
});
it('should set "plan" attributes', () => {
expect(state.plan.code).toBe(payload.plan.code);
expect(state.plan.name).toBe(payload.plan.name);
expect(state.plan.trial).toBe(payload.plan.trial);
});
it('should populate "subscriptionStartDate" and "subscriptionEndDate" from "billings row" correctly', () => {
const usageRow = state.tables.trial.rows[0];
const data = convertObjectPropsToCamelCase(payload, { deep: true });
usageRow.columns.forEach(column => {
if (column.id === 'subscriptionStartDate') {
expect(column.value).toBe(data.billing.subscriptionStartDate);
} else if (column.id === 'subscriptionEndDate') {
expect(column.value).toBe(data.billing.subscriptionEndDate);
}
});
});
});
});
......
......@@ -8406,9 +8406,18 @@ msgstr ""
msgid "SubscriptionTable|This is the next date when the GitLab.com team is scheduled to get in contact with you to settle any outstanding balances."
msgstr ""
msgid "SubscriptionTable|This is the number of seats you will be required to purchase if you update to a paid plan."
msgstr ""
msgid "SubscriptionTable|Trial"
msgstr ""
msgid "SubscriptionTable|Trial end date"
msgstr ""
msgid "SubscriptionTable|Trial start date"
msgstr ""
msgid "SubscriptionTable|Upgrade"
msgstr ""
......@@ -8418,9 +8427,6 @@ msgstr ""
msgid "SubscriptionTable|Usage count is performed once a day at 12:00 PM."
msgstr ""
msgid "SubscriptionTable|subscription"
msgstr ""
msgid "Suggested change"
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